diff --git a/.env.example b/.env.example index e66f50ea..e48efde4 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,10 @@ JWT_SECRET=please-change-me-32-chars-minimum-xxxx JWT_ALGORITHM=HS256 LOG_LEVEL=INFO +# 多用户登录开关:dashboard 读。false/不设(本地)=不登录回落 CONSOLE_SUBJECT; +# true(线上)=强制登录。账号存 DB(users 表),用 services/paper/scripts/create_user.py 种入。 +AUTH_ENABLED=false + # ───── 2. Service URLs & ports(本地默认) ───────────────── DATA_SERVICE_URL=http://localhost:8001 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b491c37d..0f948d82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,6 +92,8 @@ jobs: run: pnpm install --frozen-lockfile - name: Type check run: pnpm typecheck + - name: Unit tests + run: pnpm test - name: Build (Next.js dynamic app) run: pnpm build diff --git a/apps/dashboard/.env.local.example b/apps/dashboard/.env.local.example index 4eb0a4dc..f48b3192 100644 --- a/apps/dashboard/.env.local.example +++ b/apps/dashboard/.env.local.example @@ -9,6 +9,9 @@ # CONSOLE_SUBJECT=console:dev # CONSOLE_EMAIL=console@inalpha.dev +# ── 多用户登录。默认关;本地联调登录闸门再设 true(需先 migrate 建 users 表 + create_user)── +# AUTH_ENABLED=false + # ── 指向远端 / 非默认端口的后端 ── # PAPER_SERVICE_URL=http://127.0.0.1:8002 # DATA_SERVICE_URL=http://127.0.0.1:8001 diff --git a/apps/dashboard/messages/en.json b/apps/dashboard/messages/en.json index af2b9c0c..bcf97d6c 100644 --- a/apps/dashboard/messages/en.json +++ b/apps/dashboard/messages/en.json @@ -16,7 +16,8 @@ "menu": "Menu", "close": "Close", "collapse": "Collapse sidebar", - "expand": "Expand sidebar" + "expand": "Expand sidebar", + "logout": "Sign out" }, "theme": { "label": "Theme", diff --git a/apps/dashboard/messages/zh.json b/apps/dashboard/messages/zh.json index 81c2db03..612ca47b 100644 --- a/apps/dashboard/messages/zh.json +++ b/apps/dashboard/messages/zh.json @@ -16,7 +16,8 @@ "menu": "菜单", "close": "关闭", "collapse": "收起侧边栏", - "expand": "展开侧边栏" + "expand": "展开侧边栏", + "logout": "登出" }, "theme": { "label": "主题", diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index b079dc10..4ef7f3d8 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -13,7 +13,8 @@ "dev": "next dev -p 3001", "build": "next build", "start": "next start -p 3001", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest run" }, "dependencies": { "@ag-ui/mastra": "^1.0.3", @@ -45,6 +46,7 @@ "@types/react-dom": "^19.2.3", "postcss": "^8.5.15", "tailwindcss": "^4.3.0", - "typescript": "^6.0.3" + "typescript": "^6.0.3", + "vitest": "^4.1.7" } } diff --git a/apps/dashboard/pnpm-lock.yaml b/apps/dashboard/pnpm-lock.yaml index 88e7e230..4240d6d5 100644 --- a/apps/dashboard/pnpm-lock.yaml +++ b/apps/dashboard/pnpm-lock.yaml @@ -97,6 +97,9 @@ importers: typescript: specifier: ^6.0.3 version: 6.0.3 + vitest: + specifier: ^4.1.7 + version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(vite@8.1.2(@types/node@25.9.1)(jiti@2.7.0)) packages: @@ -348,9 +351,18 @@ packages: resolution: {integrity: sha512-LDyFmSr53j3AGxvca9yMsJIAVnpzbAueftgKy7/Jcqk5rsiaFLSlQGOJyYY8AV5QhQ0JANoAzx47GTxqVWKJmg==} engines: {node: '>=18'} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@envelop/core@5.5.1': resolution: {integrity: sha512-3DQg8sFskDo386TkL5j12jyRAdip/8yzK3x7YGbZBgobZ4aKXrvDU0GppU0SnmrpQnNaiTUsxBs9LKkwQ/eyvw==} engines: {node: '>=18.0.0'} @@ -730,6 +742,12 @@ packages: '@cfworker/json-schema': optional: true + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@next/env@16.2.7': resolution: {integrity: sha512-tMJizPlj6ZYpBMMdK8S0LJufrP4QTdR6pcv9KQ/bVETPAmg0j1mlHE9G2c38UyGHxoBapgwuj7XjbGJ2RcDFOg==} @@ -789,6 +807,9 @@ packages: resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} + '@oxc-project/types@0.137.0': + resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} engines: {node: '>= 10.0.0'} @@ -1200,6 +1221,104 @@ packages: '@repeaterjs/repeater@3.0.6': resolution: {integrity: sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==} + '@rolldown/binding-android-arm64@1.1.3': + resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.3': + resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.3': + resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.3': + resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.3': + resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.3': + resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.3': + resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.3': + resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.3': + resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.3': + resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.3': + resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.3': + resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.3': + resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.3': + resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} @@ -1464,6 +1583,12 @@ packages: '@tanstack/virtual-core@3.17.0': resolution: {integrity: sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -1560,6 +1685,9 @@ packages: '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -1635,6 +1763,35 @@ packages: resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} engines: {node: '>= 20'} + '@vitest/expect@4.1.9': + resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} + + '@vitest/mocker@4.1.9': + resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.9': + resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} + + '@vitest/runner@4.1.9': + resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} + + '@vitest/snapshot@4.1.9': + resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} + + '@vitest/spy@4.1.9': + resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} + + '@vitest/utils@4.1.9': + resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} + '@whatwg-node/disposablestack@0.0.6': resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==} engines: {node: '>=18.0.0'} @@ -1709,6 +1866,10 @@ packages: array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -1762,6 +1923,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1850,6 +2015,9 @@ packages: resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} engines: {node: '>=18'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} @@ -2148,6 +2316,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@2.2.0: + resolution: {integrity: sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==} + es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} @@ -2170,6 +2341,9 @@ packages: estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -2200,6 +2374,10 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + express-rate-limit@8.5.2: resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} engines: {node: '>= 16'} @@ -2242,6 +2420,15 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} @@ -2288,6 +2475,11 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -3195,6 +3387,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -3288,6 +3484,9 @@ packages: path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + phoenix@1.8.7: resolution: {integrity: sha512-mQfiO4PNEjToj5iUfka6OjRWtCNe+0JsVrW6x+q94kPN8VX8taulJPTa4/u2ZPb2b8+hzmBAvMxQWnuTc0T4KQ==} @@ -3333,6 +3532,10 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + engines: {node: ^10 || ^12 || >=14} + posthog-node@5.36.0: resolution: {integrity: sha512-l7NQY9SggkyFZQJzWVeE3aPbo6CBVftfs5ZRjUP4yMUKPPxXaYst2aaNjSUlMYs2+oZIA0suVfrKFt4DuZpu5A==} engines: {node: ^20.20.0 || >=22.22.0} @@ -3535,6 +3738,11 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + rolldown@1.1.3: + resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} @@ -3635,6 +3843,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -3656,10 +3867,16 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + streamdown@1.6.11: resolution: {integrity: sha512-Y38fwRx5kCKTluwM+Gf27jbbi9q6Qy+WC9YrC1YbCpMkktT3PsRBJHMWiqYeF8y/JzLpB1IzDoeaB6qkQEDnAA==} peerDependencies: @@ -3730,10 +3947,21 @@ packages: thread-stream@3.2.0: resolution: {integrity: sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@1.2.4: resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -3932,6 +4160,90 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@8.1.2: + resolution: {integrity: sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.9: + resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.9 + '@vitest/browser-preview': 4.1.9 + '@vitest/browser-webdriverio': 4.1.9 + '@vitest/coverage-istanbul': 4.1.9 + '@vitest/coverage-v8': 4.1.9 + '@vitest/ui': 4.1.9 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} @@ -3950,6 +4262,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wonka@6.3.6: resolution: {integrity: sha512-MXH+6mDHAZ2GuMpgKS055FR6v0xVP3XwquxIMYXgiW+FejHQlMGlvVRZT4qMCxR+bEo/FCtIdKxwej9WV3YQag==} @@ -4411,11 +4728,27 @@ snapshots: - encoding - zod + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@envelop/core@5.5.1': dependencies: '@envelop/instrumentation': 1.0.0 @@ -4858,6 +5191,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + '@next/env@16.2.7': {} '@next/swc-darwin-arm64@16.2.7': @@ -4886,6 +5226,8 @@ snapshots: '@opentelemetry/api@1.9.1': {} + '@oxc-project/types@0.137.0': {} + '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -5238,6 +5580,57 @@ snapshots: '@repeaterjs/repeater@3.0.6': {} + '@rolldown/binding-android-arm64@1.1.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.3': + optional: true + + '@rolldown/binding-darwin-x64@1.1.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.3': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.3': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + '@scarf/scarf@1.4.0': {} '@schummar/icu-type-parser@1.21.5': {} @@ -5463,6 +5856,16 @@ snapshots: '@tanstack/virtual-core@3.17.0': {} + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/d3-array@3.2.2': {} '@types/d3-axis@3.0.6': @@ -5584,6 +5987,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.9 @@ -5656,6 +6061,47 @@ snapshots: '@vercel/oidc@3.2.0': {} + '@vitest/expect@4.1.9': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.9(vite@8.1.2(@types/node@25.9.1)(jiti@2.7.0))': + dependencies: + '@vitest/spy': 4.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.2(@types/node@25.9.1)(jiti@2.7.0) + + '@vitest/pretty-format@4.1.9': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.9': + dependencies: + '@vitest/utils': 4.1.9 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + '@vitest/utils': 4.1.9 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.9': {} + + '@vitest/utils@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@whatwg-node/disposablestack@0.0.6': dependencies: '@whatwg-node/promise-helpers': 1.3.2 @@ -5749,6 +6195,8 @@ snapshots: array-flatten@1.1.1: {} + assertion-error@2.0.1: {} + atomic-sleep@1.0.0: {} bail@2.0.2: {} @@ -5815,6 +6263,8 @@ snapshots: ccount@2.0.1: {} + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -5887,6 +6337,8 @@ snapshots: content-type@2.0.0: {} + convert-source-map@2.0.0: {} + cookie-signature@1.0.7: {} cookie-signature@1.2.2: {} @@ -6181,6 +6633,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@2.2.0: {} + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -6195,6 +6649,10 @@ snapshots: estree-util-is-identifier-name@3.0.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + etag@1.8.1: {} event-target-shim@5.0.1: {} @@ -6226,6 +6684,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + expect-type@1.4.0: {} + express-rate-limit@8.5.2(express@5.2.1): dependencies: express: 5.2.1 @@ -6322,6 +6782,10 @@ snapshots: dependencies: reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 @@ -6373,6 +6837,9 @@ snapshots: fresh@2.0.0: {} + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} gaxios@7.1.4: @@ -7621,6 +8088,8 @@ snapshots: object-inspect@1.13.4: {} + obug@2.1.3: {} + on-exit-leak-free@2.1.2: {} on-finished@2.4.1: @@ -7700,6 +8169,8 @@ snapshots: path-to-regexp@8.4.2: {} + pathe@2.0.3: {} + phoenix@1.8.7: {} picocolors@1.1.1: {} @@ -7766,6 +8237,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.16: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + posthog-node@5.36.0(rxjs@7.8.1): dependencies: '@posthog/core': 1.30.6 @@ -8035,6 +8512,27 @@ snapshots: robust-predicates@3.0.3: {} + rolldown@1.1.3: + dependencies: + '@oxc-project/types': 0.137.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.3 + '@rolldown/binding-darwin-arm64': 1.1.3 + '@rolldown/binding-darwin-x64': 1.1.3 + '@rolldown/binding-freebsd-x64': 1.1.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.3 + '@rolldown/binding-linux-arm64-gnu': 1.1.3 + '@rolldown/binding-linux-arm64-musl': 1.1.3 + '@rolldown/binding-linux-ppc64-gnu': 1.1.3 + '@rolldown/binding-linux-s390x-gnu': 1.1.3 + '@rolldown/binding-linux-x64-gnu': 1.1.3 + '@rolldown/binding-linux-x64-musl': 1.1.3 + '@rolldown/binding-openharmony-arm64': 1.1.3 + '@rolldown/binding-wasm32-wasi': 1.1.3 + '@rolldown/binding-win32-arm64-msvc': 1.1.3 + '@rolldown/binding-win32-x64-msvc': 1.1.3 + roughjs@4.6.6: dependencies: hachure-fill: 0.5.2 @@ -8215,6 +8713,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@4.1.0: {} sonic-boom@4.2.1: @@ -8229,8 +8729,12 @@ snapshots: sprintf-js@1.0.3: {} + stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@4.1.0: {} + streamdown@1.6.11(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.7): dependencies: clsx: 2.1.1 @@ -8317,8 +8821,17 @@ snapshots: dependencies: real-require: 0.2.0 + tinybench@2.9.0: {} + tinyexec@1.2.4: {} + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + toidentifier@1.0.1: {} tokenx@1.3.0: {} @@ -8532,6 +9045,46 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + vite@8.1.2(@types/node@25.9.1)(jiti@2.7.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.16 + rolldown: 1.1.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 25.9.1 + fsevents: 2.3.3 + jiti: 2.7.0 + + vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(vite@8.1.2(@types/node@25.9.1)(jiti@2.7.0)): + dependencies: + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(vite@8.1.2(@types/node@25.9.1)(jiti@2.7.0)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + es-module-lexer: 2.2.0 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.2(@types/node@25.9.1)(jiti@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 25.9.1 + transitivePeerDependencies: + - msw + web-namespaces@2.0.1: {} web-streams-polyfill@3.3.3: {} @@ -8547,6 +9100,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wonka@6.3.6: {} wrappy@1.0.2: {} diff --git a/apps/dashboard/src/app/api/auth/login/route.ts b/apps/dashboard/src/app/api/auth/login/route.ts new file mode 100644 index 00000000..80fcc1af --- /dev/null +++ b/apps/dashboard/src/app/api/auth/login/route.ts @@ -0,0 +1,60 @@ +import { NextResponse } from "next/server"; + +import { BackendError, backendFetch } from "@/lib/backend"; +import { + SESSION_COOKIE, + SESSION_COOKIE_OPTS, + SESSION_TTL_SEC, + createSessionToken, +} from "@/lib/session"; + +/** + * 登录:校验凭据 → 落 session cookie。 + * + * dashboard 无 DB 凭据,把邮箱 / 密码反代到内网 paper `/auth/login` 校验;成功后用 + * `JWT_SECRET` 签 httpOnly session cookie。密码只透传一次,不落任何日志。 + */ +export async function POST(req: Request): Promise { + let email: unknown; + let password: unknown; + try { + ({ email, password } = await req.json()); + } catch { + return NextResponse.json({ error: "请求体格式错误" }, { status: 400 }); + } + if (typeof email !== "string" || typeof password !== "string" || !email || !password) { + return NextResponse.json({ error: "缺少邮箱或密码" }, { status: 400 }); + } + + try { + const user = await backendFetch<{ subject: string; email: string; roles: string[] }>( + "paper", + "/auth/login", + { auth: false, method: "POST", body: { email, password } }, + ); + const token = await createSessionToken({ + subject: user.subject, + email: user.email, + roles: user.roles ?? [], + }); + const res = NextResponse.json({ ok: true }); + res.cookies.set(SESSION_COOKIE, token, { + ...SESSION_COOKIE_OPTS, + maxAge: SESSION_TTL_SEC, + }); + return res; + } catch (err) { + if (err instanceof BackendError && err.status === 401) { + return NextResponse.json({ error: "邮箱或密码不正确" }, { status: 401 }); + } + // 透传 paper 的失败节流(429),否则会被误报成"登录服务不可用"(502), + // 用户看不到"尝试过于频繁"的真实原因。 + if (err instanceof BackendError && err.status === 429) { + return NextResponse.json( + { error: "尝试过于频繁,请稍后再试" }, + { status: 429 }, + ); + } + return NextResponse.json({ error: "登录服务暂不可用,请稍后重试" }, { status: 502 }); + } +} diff --git a/apps/dashboard/src/app/api/auth/logout/route.ts b/apps/dashboard/src/app/api/auth/logout/route.ts new file mode 100644 index 00000000..c14cdd8d --- /dev/null +++ b/apps/dashboard/src/app/api/auth/logout/route.ts @@ -0,0 +1,10 @@ +import { NextResponse } from "next/server"; + +import { SESSION_COOKIE, SESSION_COOKIE_OPTS } from "@/lib/session"; + +/** 登出:清 session cookie。前端随后跳 /login。 */ +export async function POST(): Promise { + const res = NextResponse.json({ ok: true }); + res.cookies.set(SESSION_COOKIE, "", { ...SESSION_COOKIE_OPTS, maxAge: 0 }); + return res; +} diff --git a/apps/dashboard/src/app/api/auth/session/route.ts b/apps/dashboard/src/app/api/auth/session/route.ts new file mode 100644 index 00000000..9b9ce461 --- /dev/null +++ b/apps/dashboard/src/app/api/auth/session/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; + +import { readSession } from "@/lib/session"; + +/** + * 当前登录用户(供侧栏显示 email + 登出按钮判存在)。未登录 / 未启用登录 → `{ user: null }`。 + * 不返回任何凭据。 + */ +export async function GET(): Promise { + const session = await readSession(); + return NextResponse.json({ + user: session ? { email: session.email, subject: session.subject } : null, + }); +} diff --git a/apps/dashboard/src/app/api/copilotkit/route.ts b/apps/dashboard/src/app/api/copilotkit/route.ts index aab7c6aa..ea18ed0e 100644 --- a/apps/dashboard/src/app/api/copilotkit/route.ts +++ b/apps/dashboard/src/app/api/copilotkit/route.ts @@ -7,7 +7,7 @@ import { import { getRemoteAgents } from "@ag-ui/mastra"; import { MastraClient } from "@mastra/client-js"; -import { BACKENDS, CONSOLE_SUBJECT, getServiceToken } from "@/lib/backend"; +import { BACKENDS, getServiceToken, getSessionSubject } from "@/lib/backend"; /** * 静音 @ag-ui/mastra 1.0.3 的良性日志噪音:它不认 mastra(v5 streamVNext)的 @@ -41,9 +41,9 @@ if (!(console.warn as { __inalphaFiltered?: boolean }).__inalphaFiltered) { * dashboard 不背 mastra 依赖树。 * 2. **每请求**重建 runtime,从而每次拿新鲜 JWT(`getServiceToken` 进程内缓存,到期前 60s 续签), * 规避长连接里 token 过期。 - * 3. **隔离**:`getRemoteAgents` 强制 `resourceId`(= JWT.sub = CONSOLE_SUBJECT),`threadId` 由前端 - * `` 传下并经 AG-UI 转发给 `agent.stream`,共同满足 memory.ts 的 - * `assertScopedRequest`。单租户 dev 下 resourceId 固定;接真实多租户时改为从 session 派生。 + * 3. **隔离**:`getRemoteAgents` 强制 `resourceId`(= 登录用户 sub,经 getSessionSubject() + * 从 session 派生;dev 未登录回落 console:dev),`threadId` 由前端 `` + * 传下并经 AG-UI 转发给 `agent.stream`,共同满足 memory.ts 的 `assertScopedRequest`。 * 4. LLM 在 mastra 侧,本层不需要 model adapter —— 用 `ExperimentalEmptyAdapter` 占位。 * * ⚠️ **已知版本约束**:`@ag-ui/mastra`(目前最新 1.0.3)的 peerDependencies 把 @@ -57,7 +57,10 @@ if (!(console.warn as { __inalphaFiltered?: boolean }).__inalphaFiltered) { * @returns CopilotKit runtime 的 POST handler */ export const POST = async (req: Request): Promise => { + // token 的 sub = 登录用户(或 dev 下 console:dev)。mastra identityMiddleware 据此 + // 注入 authSub,tool 层再据此打给 Python(resolveRequestToken),保证 agent 写操作落登录用户账户。 const token = await getServiceToken(); + const resourceId = await getSessionSubject(); const mastraClient = new MastraClient({ baseUrl: BACKENDS.mastra, @@ -66,7 +69,7 @@ export const POST = async (req: Request): Promise => { const agents = await getRemoteAgents({ mastraClient, - resourceId: CONSOLE_SUBJECT, + resourceId, }); // CopilotKit 1.59.5 起 `agents` 收紧为 NonEmptyRecord | Promise | factory; diff --git a/apps/dashboard/src/app/api/divination/history/route.ts b/apps/dashboard/src/app/api/divination/history/route.ts index 88a61875..cf4d0ab3 100644 --- a/apps/dashboard/src/app/api/divination/history/route.ts +++ b/apps/dashboard/src/app/api/divination/history/route.ts @@ -1,21 +1,21 @@ import { NextRequest, NextResponse } from "next/server"; -import { BackendError, backendFetch, CONSOLE_SUBJECT } from "@/lib/backend"; +import { BackendError, backendFetch, getSessionSubject } from "@/lib/backend"; export const dynamic = "force-dynamic"; /** * GET /api/divination/history?limit= —— 占卜台历史记录(BFF)。 * - * 转发到 mastra 的 `GET /divination/history`,注入 `subject = CONSOLE_SUBJECT` - * (只看当前控制台身份的记录)。limit 透传(mastra 侧封顶 100)。 + * 转发到 mastra 的 `GET /divination/history`,注入 `subject`(经 getSessionSubject() + * 从登录用户派生,dev 回落 console:dev,只看本人记录)。limit 透传(mastra 侧封顶 100)。 */ export async function GET(req: NextRequest) { const limit = req.nextUrl.searchParams.get("limit") ?? undefined; try { const data = await backendFetch<{ records: unknown[] }>("mastra", "/divination/history", { timeoutMs: 8000, - query: { subject: CONSOLE_SUBJECT, limit }, + query: { subject: await getSessionSubject(), limit }, }); return NextResponse.json(data); } catch (err) { diff --git a/apps/dashboard/src/app/api/divination/route.ts b/apps/dashboard/src/app/api/divination/route.ts index 7b7ac95d..003d6c92 100644 --- a/apps/dashboard/src/app/api/divination/route.ts +++ b/apps/dashboard/src/app/api/divination/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { BackendError, backendFetch, CONSOLE_SUBJECT } from "@/lib/backend"; +import { BackendError, backendFetch, getSessionSubject } from "@/lib/backend"; export const dynamic = "force-dynamic"; @@ -8,7 +8,7 @@ export const dynamic = "force-dynamic"; * POST /api/divination —— 占卜台直算端点(BFF)。 * * 转发到 mastra 的 `POST /divination/cast`(纯计算、**无 LLM**、确定性),注入控制台 - * 身份 `subject = CONSOLE_SUBJECT` 做历史隶属。结果由 mastra 落库,这里只透传返回。 + * 身份 `subject`(经 getSessionSubject() 从登录用户派生,dev 回落 console:dev)做历史隶属。结果由 mastra 落库,这里只透传返回。 * * 设计:狐神签独立模块点按钮 → 本路由 → mastra 直算 → 瞬时出卦,**不触发对话栏会话**; * 会话式深度解读由用户主动在对话栏触发(走 `/api/copilotkit`)。 @@ -29,7 +29,7 @@ export async function POST(req: NextRequest) { mode: body["mode"], question: body["question"], symbol: body["symbol"], - subject: CONSOLE_SUBJECT, + subject: await getSessionSubject(), }, }); return NextResponse.json(record); diff --git a/apps/dashboard/src/app/api/factors/candidates/[id]/route.ts b/apps/dashboard/src/app/api/factors/candidates/[id]/route.ts index f3656bc4..7a9fa195 100644 --- a/apps/dashboard/src/app/api/factors/candidates/[id]/route.ts +++ b/apps/dashboard/src/app/api/factors/candidates/[id]/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { CONSOLE_SUBJECT, backendFetch } from "@/lib/backend"; +import { backendFetch, getSessionSubject } from "@/lib/backend"; import type { FactorCandidate } from "@/lib/types"; export const dynamic = "force-dynamic"; @@ -35,11 +35,9 @@ export async function POST( auth: false, method: "POST", body: { - // 审计可追溯:从控制台账户身份派生,别硬编码占位符——否则所有审核记录 - // 都标同一 "console:dev",事后复盘分不清谁批的。单租户 dev 下 = CONSOLE_SUBJECT; - // 接 session 鉴权后改为从 JWT 派生(同 backend.ts CONSOLE_SUBJECT 约定)。 + // 审计可追溯:从登录用户身份派生(dev 未登录回落 console:dev),别硬编码占位符。 action: body.action, - reviewed_by: CONSOLE_SUBJECT, + reviewed_by: await getSessionSubject(), note: body.note ?? null, }, timeoutMs: 8000, diff --git a/apps/dashboard/src/app/login/page.tsx b/apps/dashboard/src/app/login/page.tsx new file mode 100644 index 00000000..6dc68c35 --- /dev/null +++ b/apps/dashboard/src/app/login/page.tsx @@ -0,0 +1,23 @@ +import { Suspense } from "react"; + +import { LoginForm } from "@/components/auth/LoginForm"; + +/** + * 登录页。刻意放在 `[locale]` 外壳之外 —— 不套控制台侧栏 / 对话栏 / 活动日志, + * 避免未登录时这些组件挂载后打 401。middleware 未登录时重定向到这里。 + */ +export const metadata = { + title: "Sign in · Inalpha", + robots: { index: false, follow: false }, +}; + +export default function LoginPage() { + return ( +
+ {/* useSearchParams 需要 Suspense 边界。 */} + + + +
+ ); +} diff --git a/apps/dashboard/src/components/auth/LoginForm.tsx b/apps/dashboard/src/components/auth/LoginForm.tsx new file mode 100644 index 00000000..80d36aac --- /dev/null +++ b/apps/dashboard/src/components/auth/LoginForm.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; + +/** + * 登录表单。登录页在 `[locale]` 外壳之外(不套控制台侧栏 / 对话栏 / intl provider), + * 故文案在此按 `navigator.language` 做最小中英切换,不依赖 next-intl。 + */ + +const STRINGS = { + en: { + title: "Operator Console", + subtitle: "Sign in to continue", + email: "Email", + password: "Password", + submit: "Sign in", + submitting: "Signing in…", + invalid: "Incorrect email or password", + rateLimited: "Too many attempts, try again later", + unavailable: "Login service unavailable, try again later", + }, + zh: { + title: "操作控制台", + subtitle: "登录以继续", + email: "邮箱", + password: "密码", + submit: "登录", + submitting: "登录中…", + invalid: "邮箱或密码不正确", + rateLimited: "尝试过于频繁,请稍后再试", + unavailable: "登录服务暂不可用,请稍后重试", + }, +}; + +function pickLang(): "en" | "zh" { + if (typeof navigator !== "undefined" && navigator.language?.toLowerCase().startsWith("zh")) { + return "zh"; + } + return "en"; +} + +export function LoginForm() { + const router = useRouter(); + const params = useSearchParams(); + const t = STRINGS[pickLang()]; + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + async function onSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setLoading(true); + try { + const res = await fetch("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + if (res.ok) { + const from = params.get("from"); + // 只接受站内相对路径,防开放重定向。 + const dest = from && from.startsWith("/") && !from.startsWith("//") ? from : "/"; + router.replace(dest); + router.refresh(); + return; + } + setError( + res.status === 401 + ? t.invalid + : res.status === 429 + ? t.rateLimited + : t.unavailable, + ); + } catch { + setError(t.unavailable); + } finally { + setLoading(false); + } + } + + return ( +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Inalpha +
+
Inalpha
+
+ {t.title} +
+
+
+ +

{t.subtitle}

+ +
+ + +
+ + {error && ( +

+ {error} +

+ )} + + +
+ ); +} diff --git a/apps/dashboard/src/components/shell/AccountControl.tsx b/apps/dashboard/src/components/shell/AccountControl.tsx new file mode 100644 index 00000000..64c1b8be --- /dev/null +++ b/apps/dashboard/src/components/shell/AccountControl.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { LogOut } from "lucide-react"; +import { useTranslations } from "next-intl"; + +import { cn } from "@/lib/cn"; + +/** + * 侧栏底部账户控件:显示登录用户邮箱 + 登出。 + * + * 未登录 / 未启用登录(`/api/auth/session` 返 `user: null`)时不渲染 —— 本地 dev 无登录态, + * 侧栏保持原样。登出后跳 `/login`(站点根路径,不带 locale 前缀,故用 next/navigation)。 + */ +export function AccountControl({ collapsed }: { collapsed: boolean }) { + const t = useTranslations("nav"); + const router = useRouter(); + const [email, setEmail] = useState(null); + + useEffect(() => { + let alive = true; + fetch("/api/auth/session") + .then((r) => (r.ok ? r.json() : null)) + .then((d) => { + if (alive) setEmail(d?.user?.email ?? null); + }) + .catch(() => {}); + return () => { + alive = false; + }; + }, []); + + if (!email) return null; + + async function logout() { + await fetch("/api/auth/logout", { method: "POST" }).catch(() => {}); + router.replace("/login"); + router.refresh(); + } + + if (collapsed) { + return ( + + ); + } + + return ( +
+ + {email} + + +
+ ); +} diff --git a/apps/dashboard/src/components/shell/ConsoleSidebar.tsx b/apps/dashboard/src/components/shell/ConsoleSidebar.tsx index d423d31a..1d057d5e 100644 --- a/apps/dashboard/src/components/shell/ConsoleSidebar.tsx +++ b/apps/dashboard/src/components/shell/ConsoleSidebar.tsx @@ -20,6 +20,7 @@ import { import { Link, usePathname } from "@/i18n/navigation"; import { cn } from "@/lib/cn"; +import { AccountControl } from "./AccountControl"; import { LocaleSwitcher } from "./LocaleSwitcher"; import { ThemeToggle } from "./ThemeToggle"; @@ -374,6 +375,7 @@ function SidebarBody({ {/* Footer —— 控制区(主题 / 语言)+ 折叠开关 + build 标记。 */} {collapsed ? (
+ {onToggleCollapsed && (