diff --git a/.gitignore b/.gitignore index 8242316..e9b2c5b 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,7 @@ test-results # Local HTTPS development certificates (generated by dev:https). .certs/ .understand-anything/ + + +#Ignore cursor AI rules +.cursor/rules/codacy.mdc diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..4954f64 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,76 @@ +import { NextRequest, NextResponse } from "next/server"; + +const ACCESS_TOKEN_COOKIE = "claw3d-access"; +const ACCESS_TOKEN_PARAM = "access_token"; + +/** + * Middleware that protects hosted Claw3D workspaces with a token-based + * access gate. When CLAW3D_ACCESS_TOKEN is set (managed deployments), + * every request must carry a valid cookie. If the cookie is missing, + * the middleware checks for a query-string token and sets the cookie + * on match; otherwise it returns a 403 page. + * + * When CLAW3D_ACCESS_TOKEN is not set (local / self-hosted), the + * middleware is a no-op. + */ +export function middleware(request: NextRequest) { + const expectedToken = process.env.CLAW3D_ACCESS_TOKEN; + + // Self-hosted mode: no gate. + if (!expectedToken) { + return NextResponse.next(); + } + + // Allow health checks and API routes used by the control plane. + const { pathname } = request.nextUrl; + if ( + pathname.startsWith("/api/health") || + pathname.startsWith("/api/managed") || + pathname.startsWith("/_next") || + pathname === "/favicon.ico" + ) { + return NextResponse.next(); + } + + // Check cookie first. + const cookieToken = request.cookies.get(ACCESS_TOKEN_COOKIE)?.value; + if (cookieToken === expectedToken) { + return NextResponse.next(); + } + + // Accept token from query string and set a session cookie. + const queryToken = request.nextUrl.searchParams.get(ACCESS_TOKEN_PARAM); + if (queryToken === expectedToken) { + const cleanUrl = request.nextUrl.clone(); + cleanUrl.searchParams.delete(ACCESS_TOKEN_PARAM); + const response = NextResponse.redirect(cleanUrl); + response.cookies.set(ACCESS_TOKEN_COOKIE, expectedToken, { + httpOnly: true, + secure: request.nextUrl.protocol === "https:", + sameSite: "lax", + path: "/", + maxAge: 60 * 60 * 24 * 30, + }); + return response; + } + + // No valid credential -- block access. + return new NextResponse( + ` + +Access required + + +

Access required

Open this workspace from your Claw3D dashboard to authenticate.

+`, + { + status: 403, + headers: { "Content-Type": "text/html; charset=utf-8" }, + }, + ); +} + +export const config = { + matcher: ["/((?!_next/static|_next/image|favicon\\.ico).*)"], +}; diff --git a/package-lock.json b/package-lock.json index 6adcc47..3e18299 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,17 @@ { "name": "claw3d", - "version": "0.1.0", + "version": "0.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claw3d", - "version": "0.1.0", + "version": "0.1.5", "license": "MIT", "dependencies": { "@noble/ed25519": "^3.0.0", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.5.0", - "@vercel/otel": "^2.1.0", "canvas-confetti": "^1.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -163,7 +162,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -470,7 +468,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -511,7 +508,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1959,141 +1955,6 @@ "node": ">=12.4.0" } }, - "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/api-logs": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.211.0.tgz", - "integrity": "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/instrumentation": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.211.0.tgz", - "integrity": "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@opentelemetry/api-logs": "0.211.0", - "import-in-the-middle": "^2.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/resources": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", - "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-logs": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.211.0.tgz", - "integrity": "sha512-O5nPwzgg2JHzo59kpQTPUOTzFi0Nv5LxryG27QoXBciX3zWM3z83g+SNOHhiQVYRWFSxoWn1JM2TGD5iNjOwdA==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@opentelemetry/api-logs": "0.211.0", - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", - "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", - "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz", - "integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, "node_modules/@peculiar/asn1-cms": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", @@ -2259,7 +2120,6 @@ "integrity": "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "playwright": "1.58.0" }, @@ -2315,7 +2175,6 @@ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz", "integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", @@ -3069,6 +2928,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3089,6 +2949,7 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -3170,7 +3031,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/canvas-confetti": { "version": "1.9.0", @@ -3286,7 +3148,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz", "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3297,7 +3158,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3322,7 +3182,6 @@ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz", "integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==", "license": "MIT", - "peer": true, "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", @@ -3400,7 +3259,6 @@ "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", @@ -3918,24 +3776,6 @@ "react": ">= 16.8.0" } }, - "node_modules/@vercel/otel": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vercel/otel/-/otel-2.1.0.tgz", - "integrity": "sha512-Zwu2Cu4t46DzBnY1DQSTxZ4MBLVfYsOjnlWuZuLRWnmVPX+SNrVHbs3ssiJ6uvY1J1JJswor4zSn8mHYxzYeBA==", - "license": "MIT", - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <2.0.0", - "@opentelemetry/api-logs": ">=0.200.0 <0.300.0", - "@opentelemetry/instrumentation": ">=0.200.0 <0.300.0", - "@opentelemetry/resources": ">=2.0.0 <3.0.0", - "@opentelemetry/sdk-logs": ">=0.200.0 <0.300.0", - "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", - "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0" - } - }, "node_modules/@vitest/expect": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", @@ -4057,8 +3897,8 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4066,15 +3906,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -4118,6 +3949,7 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -4495,7 +4327,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4724,18 +4555,11 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/cjs-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", - "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", - "license": "MIT" - }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "clsx": "^2.1.1" }, @@ -4754,7 +4578,6 @@ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -5112,7 +4935,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/draco3d": { "version": "1.5.7", @@ -5431,7 +5255,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5633,7 +5456,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6532,18 +6354,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-in-the-middle": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.5.tgz", - "integrity": "sha512-0InH9/4oDCBRzWXhpOqusspLBrVfK1vPvbn9Wxl8DAQ8yyx5fWJRETICSwkiAMaYntjJAMBP1R4B6cQnEUYVEA==", - "license": "Apache-2.0", - "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^2.2.0", - "module-details-from-path": "^1.0.4" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -7148,7 +6958,6 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -7631,6 +7440,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -8599,12 +8409,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/module-details-from-path": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", - "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", - "license": "MIT" - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9178,6 +8982,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -9193,6 +8998,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -9205,7 +9011,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/promise-worker-transferable": { "version": "1.0.4", @@ -9295,7 +9102,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9305,7 +9111,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9518,19 +9323,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-in-the-middle": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", - "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "module-details-from-path": "^1.0.3" - }, - "engines": { - "node": ">=9.3.0 || >=8.10.0 <9.0.0" - } - }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -10292,7 +10084,6 @@ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" @@ -10323,8 +10114,7 @@ "version": "0.183.2", "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/three-mesh-bvh": { "version": "0.8.3", @@ -10416,7 +10206,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10752,7 +10541,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11026,7 +10814,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -11135,7 +10922,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11487,7 +11273,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/features/onboarding/components/OnboardingWizard.tsx b/src/features/onboarding/components/OnboardingWizard.tsx index bfda466..9c28055 100644 --- a/src/features/onboarding/components/OnboardingWizard.tsx +++ b/src/features/onboarding/components/OnboardingWizard.tsx @@ -5,7 +5,7 @@ * and content slots for each onboarding phase. Designed to be mounted * at the app root and dismissed once complete or skipped. */ -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { ArrowLeft, ArrowRight, X } from "lucide-react"; import { @@ -74,6 +74,31 @@ export const OnboardingWizard = ({ ); const [managedByokReady, setManagedByokReady] = useState(false); + // In managed mode, skip straight to "company" when BYOK is already configured. + useEffect(() => { + if (!managedDeployment) return; + let cancelled = false; + + fetch("/api/managed/byok", { cache: "no-store" }) + .then((r) => r.json()) + .then((result) => { + if (cancelled) return; + const providers = Array.isArray(result.configuredProviders) + ? result.configuredProviders + : []; + if (providers.length > 0) { + setCompletedSteps(new Set(["welcome", "prerequisites", "byok"])); + setCurrentStep("company"); + setManagedByokReady(true); + } + }) + .catch(() => {}); + + return () => { + cancelled = true; + }; + }, [managedDeployment]); + const stepIndex = useMemo( () => steps.findIndex((step) => step.id === currentStep), [currentStep, steps], diff --git a/src/features/retro-office/objects/agents.tsx b/src/features/retro-office/objects/agents.tsx index 58314a9..3b0f382 100644 --- a/src/features/retro-office/objects/agents.tsx +++ b/src/features/retro-office/objects/agents.tsx @@ -6,6 +6,7 @@ import { createDefaultAgentAvatarProfile } from "@/lib/avatars/profile"; import { AGENT_SCALE, WALK_ANIM_SPEED, + WALK_SPEED, } from "@/features/retro-office/core/constants"; import { toWorld } from "@/features/retro-office/core/geometry"; import type { @@ -97,7 +98,25 @@ export const AgentModel = memo(function AgentModel({ : undefined; const workoutStyle = agent.workoutStyle ?? "lift"; const frameValue = agent.frame + (agent.phaseOffset ?? 0) / WALK_ANIM_SPEED; - const walkPhase = Math.sin(frameValue * WALK_ANIM_SPEED); + const walkSpeedRatio = THREE.MathUtils.clamp( + (agent.walkSpeed ?? WALK_SPEED) / WALK_SPEED, + 1, + 4, + ); + const walkCycle = frameValue * WALK_ANIM_SPEED * (0.8 + walkSpeedRatio * 0.12); + const walkPhase = Math.sin(walkCycle); + const walkLiftPhase = Math.abs(Math.sin(walkCycle)); + const walkSwing = THREE.MathUtils.clamp( + 0.42 + (walkSpeedRatio - 1) * 0.08, + 0.42, + 0.68, + ); + const walkLegSwing = THREE.MathUtils.clamp(walkSwing + 0.14, 0.56, 0.82); + const walkLean = THREE.MathUtils.clamp( + 0.08 + (walkSpeedRatio - 1) * 0.018, + 0.08, + 0.14, + ); const workoutPhase = Math.sin( agent.frame * 0.18 + (agent.phaseOffset ?? 0), ); @@ -108,6 +127,8 @@ export const AgentModel = memo(function AgentModel({ groupRef.current.rotation.x = agent.state === "sitting" ? -0.15 + : agent.state === "walking" + ? walkLean : isDancing ? Math.sin(agent.frame * 0.18 + (agent.phaseOffset ?? 0)) * 0.06 : isWorkout @@ -127,7 +148,7 @@ export const AgentModel = memo(function AgentModel({ : 0; const bounce = agent.state === "walking" - ? Math.sin(frameValue * WALK_ANIM_SPEED) * 0.04 + ? walkLiftPhase * 0.07 : isDancing ? 0.03 + Math.abs(Math.sin(agent.frame * 0.22 + (agent.phaseOffset ?? 0))) * @@ -153,7 +174,10 @@ export const AgentModel = memo(function AgentModel({ leftArmRef.current.rotation.x = -0.22; leftArmRef.current.rotation.z = -0.08; } else if (agent.state === "walking") { - leftArmRef.current.rotation.x = walkPhase * 0.4; + leftArmRef.current.rotation.x = walkPhase * walkSwing - 0.12; + leftArmRef.current.rotation.y = -0.04; + leftArmRef.current.rotation.z = -0.1 - walkLiftPhase * 0.05; + groupRef.current.rotation.z = Math.sin(walkCycle * 2) * 0.025; } else if (isDancing) { leftArmRef.current.rotation.x = -0.8 + Math.sin(agent.frame * 0.22) * 0.9; @@ -212,7 +236,10 @@ export const AgentModel = memo(function AgentModel({ rightArmRef.current.rotation.y = 0.18; rightArmRef.current.rotation.z = 0.08; } else if (agent.state === "walking") { - rightArmRef.current.rotation.x = -walkPhase * 0.4; + rightArmRef.current.rotation.x = -walkPhase * walkSwing - 0.12; + rightArmRef.current.rotation.y = 0.04; + rightArmRef.current.rotation.z = 0.1 + walkLiftPhase * 0.05; + groupRef.current.rotation.z = Math.sin(walkCycle * 2) * 0.025; } else if (isDancing) { rightArmRef.current.rotation.x = -0.8 - Math.sin(agent.frame * 0.22) * 0.9; @@ -265,7 +292,7 @@ export const AgentModel = memo(function AgentModel({ if (leftLegRef.current) { leftLegRef.current.rotation.x = agent.state === "walking" - ? walkPhase * 0.35 + ? walkPhase * walkLegSwing + walkLiftPhase * 0.08 : isDancing ? Math.sin(agent.frame * 0.22 + (agent.phaseOffset ?? 0)) * 0.35 : isWorkout @@ -285,7 +312,7 @@ export const AgentModel = memo(function AgentModel({ if (rightLegRef.current) { rightLegRef.current.rotation.x = agent.state === "walking" - ? -walkPhase * 0.35 + ? -walkPhase * walkLegSwing + walkLiftPhase * 0.08 : isDancing ? -Math.sin(agent.frame * 0.22 + (agent.phaseOffset ?? 0)) * 0.35 : isWorkout