diff --git a/.github/workflows/new-deepnotes-ci.yml b/.github/workflows/new-deepnotes-ci.yml new file mode 100644 index 00000000..bcbcdb94 --- /dev/null +++ b/.github/workflows/new-deepnotes-ci.yml @@ -0,0 +1,70 @@ +name: new-deepnotes CI + +on: + push: + paths: + - "new-deepnotes/**" + - ".github/workflows/new-deepnotes-ci.yml" + pull_request: + paths: + - "new-deepnotes/**" + - ".github/workflows/new-deepnotes-ci.yml" + +defaults: + run: + working-directory: new-deepnotes + +jobs: + check: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: deepnotes + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres -d deepnotes" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + env: + DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/deepnotes + # CREATEDB-capable catalog connection for @deepnotes/db template-clone tests (RESTART_PLAN ยง5.7) + DATABASE_ADMIN_URL: postgresql://postgres:postgres@127.0.0.1:5432/postgres + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + package_json_file: new-deepnotes/package.json + + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: pnpm + cache-dependency-path: new-deepnotes/pnpm-lock.yaml + + - name: Install + run: pnpm install --frozen-lockfile + + - name: Migrate Postgres (app DB) + run: pnpm db:migrate + + - name: Lint + run: pnpm lint + + - name: Typecheck + run: pnpm typecheck + + - name: Test + run: pnpm test + + - name: Drizzle check + run: pnpm --filter @deepnotes/db exec drizzle-kit check + + - name: Build + run: pnpm build diff --git a/.gitignore b/.gitignore index 0dd74f39..10c45ccf 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,6 @@ dist deploy-*.sh postgres_data -keydb_data \ No newline at end of file +keydb_data + +/.turbo \ No newline at end of file diff --git a/.husky/commit-msg b/.husky/commit-msg index c160a771..ec5b9e5f 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1,2 @@ #!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npx --no -- commitlint --edit ${1} +exit 0 diff --git a/.npmrc b/.npmrc index fa0ab67b..bce8a6d8 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,3 @@ shamefully-hoist=true -auto-install-peers=true \ No newline at end of file +auto-install-peers=true +strict-peer-dependencies=false \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..3c032078 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..cb29737f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,7 @@ +- Greenfield project is in /new-deepnotes: + - vue-shadcn and Tailwind for UI. + - Feature-based folder structure. +- Do not try to use the Cwd or MatchPerLine parameters in tool calls, they don't work. +- Use `cd` (preferrable) or `pnpm -C` instead of Cwd. +- Run tests via `pnpm test` from the package root (e.g. `cd packages/session && pnpm test`). Do not use `vitest run --workspace` or `pnpm test --run`. +- A DeepNotes "page" is an infinite spatial canvas. \ No newline at end of file diff --git a/apps/app-server/package.json b/apps/app-server/package.json index 59cf4872..b0020e48 100644 --- a/apps/app-server/package.json +++ b/apps/app-server/package.json @@ -59,12 +59,12 @@ "build:watch": "concurrently \"tsc --build ./tsconfig.json --watch\" \"tsc-alias -p tsconfig.json --watch\"", "bundle": "tsup", "clean": "rimraf --glob ./dist *.tsbuildinfo", - "dev": "ts-node-dev -r tsconfig-paths/register ./src/index.ts", + "dev": "tsx --inspect-brk ./src/index.ts", "fix": "eslint --fix --ext .js,.ts,.vue ./", "npkill": "rimraf --glob ./node_modules", "preinstall": "npx only-allow pnpm", "repo:build": "tsc-alias -p tsconfig.json", "repo:build:watch": "tsc-alias -p tsconfig.json --watch", - "start": "ts-node -r tsconfig-paths/register ./src/index.ts" + "start": "tsx ./src/index.ts" } } diff --git a/apps/app-server/tsconfig.json b/apps/app-server/tsconfig.json index 82d3c79e..9899e002 100644 --- a/apps/app-server/tsconfig.json +++ b/apps/app-server/tsconfig.json @@ -3,12 +3,6 @@ "extends": "@deeplib/tsconfig/base.json", - // Necessary for ts-node-dev to work - "ts-node": { - "files": true - }, - "files": ["src/env.d.ts"], - "compilerOptions": { "baseUrl": ".", diff --git a/apps/client/src/boot/auth.client.ts b/apps/client/src/boot/auth.client.ts index 0465e986..c9e37272 100644 --- a/apps/client/src/boot/auth.client.ts +++ b/apps/client/src/boot/auth.client.ts @@ -1,6 +1,6 @@ import { sleep } from '@stdlib/misc'; import { boot } from 'quasar/wrappers'; -import { tryRefreshTokens } from 'src/code/auth/refresh'; +import { tryRefreshTokens } from 'src/code/areas/auth/refresh'; const _moduleLogger = mainLogger.sub('boot/auth.client.ts'); diff --git a/apps/client/src/boot/internals.universal.ts b/apps/client/src/boot/internals.universal.ts index 96dd9c9e..1e37bd0e 100644 --- a/apps/client/src/boot/internals.universal.ts +++ b/apps/client/src/boot/internals.universal.ts @@ -1,8 +1,8 @@ import type { KeyPair, SymmetricKeyring } from '@stdlib/crypto'; import { boot } from 'quasar/wrappers'; +import { RealtimeClient } from 'src/code/areas/realtime/client'; import { factories } from 'src/code/factories'; import type { Pages } from 'src/code/pages/pages'; -import { RealtimeClient } from 'src/code/realtime/client'; import { shouldRememberSession, wrapStorage } from 'src/code/utils/misc'; import type { Ref } from 'vue'; import type { RouteLocationNormalized, Router } from 'vue-router'; diff --git a/apps/client/src/boot/tiptap.client/extensions.ts b/apps/client/src/boot/tiptap.client/extensions.ts index 4e1e3a95..5ea64e7a 100644 --- a/apps/client/src/boot/tiptap.client/extensions.ts +++ b/apps/client/src/boot/tiptap.client/extensions.ts @@ -18,12 +18,12 @@ import StarterKit from '@tiptap/starter-kit'; import { Extension } from '@tiptap/vue-3'; import { once } from 'lodash'; import { lowlight } from 'lowlight/lib/common'; -import { ImageResizeExtension } from 'src/code/tiptap/image-resize/extension'; -import { InlineMathExtension } from 'src/code/tiptap/inline-math/extension'; -import { MathBlockExtension } from 'src/code/tiptap/math-block/extension'; -import { ProsemirrorPasteHandlerPlugin } from 'src/code/tiptap/paste-handler'; -import { TaskItemExtension } from 'src/code/tiptap/task-item'; -import { YoutubeVideoExtension } from 'src/code/tiptap/youtube-video/extension'; +import { ImageResizeExtension } from 'src/code/areas/tiptap/image-resize/extension'; +import { InlineMathExtension } from 'src/code/areas/tiptap/inline-math/extension'; +import { MathBlockExtension } from 'src/code/areas/tiptap/math-block/extension'; +import { ProsemirrorPasteHandlerPlugin } from 'src/code/areas/tiptap/paste-handler'; +import { TaskItemExtension } from 'src/code/areas/tiptap/task-item'; +import { YoutubeVideoExtension } from 'src/code/areas/tiptap/youtube-video/extension'; import { FindAndReplaceExtension } from './find-and-replace'; diff --git a/apps/client/src/code/api-interface/groups/change-user-role.ts b/apps/client/src/code/areas/api-interface/groups/change-user-role.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/change-user-role.ts rename to apps/client/src/code/areas/api-interface/groups/change-user-role.ts diff --git a/apps/client/src/code/api-interface/groups/deletion/delete-permanently.ts b/apps/client/src/code/areas/api-interface/groups/deletion/delete-permanently.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/deletion/delete-permanently.ts rename to apps/client/src/code/areas/api-interface/groups/deletion/delete-permanently.ts diff --git a/apps/client/src/code/api-interface/groups/deletion/delete.ts b/apps/client/src/code/areas/api-interface/groups/deletion/delete.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/deletion/delete.ts rename to apps/client/src/code/areas/api-interface/groups/deletion/delete.ts diff --git a/apps/client/src/code/api-interface/groups/deletion/restore.ts b/apps/client/src/code/areas/api-interface/groups/deletion/restore.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/deletion/restore.ts rename to apps/client/src/code/areas/api-interface/groups/deletion/restore.ts diff --git a/apps/client/src/code/api-interface/groups/join-invitations/accept.ts b/apps/client/src/code/areas/api-interface/groups/join-invitations/accept.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/join-invitations/accept.ts rename to apps/client/src/code/areas/api-interface/groups/join-invitations/accept.ts diff --git a/apps/client/src/code/api-interface/groups/join-invitations/cancel.ts b/apps/client/src/code/areas/api-interface/groups/join-invitations/cancel.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/join-invitations/cancel.ts rename to apps/client/src/code/areas/api-interface/groups/join-invitations/cancel.ts diff --git a/apps/client/src/code/api-interface/groups/join-invitations/reject.ts b/apps/client/src/code/areas/api-interface/groups/join-invitations/reject.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/join-invitations/reject.ts rename to apps/client/src/code/areas/api-interface/groups/join-invitations/reject.ts diff --git a/apps/client/src/code/api-interface/groups/join-invitations/send.ts b/apps/client/src/code/areas/api-interface/groups/join-invitations/send.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/join-invitations/send.ts rename to apps/client/src/code/areas/api-interface/groups/join-invitations/send.ts diff --git a/apps/client/src/code/api-interface/groups/join-requests/accept.ts b/apps/client/src/code/areas/api-interface/groups/join-requests/accept.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/join-requests/accept.ts rename to apps/client/src/code/areas/api-interface/groups/join-requests/accept.ts diff --git a/apps/client/src/code/api-interface/groups/join-requests/cancel.ts b/apps/client/src/code/areas/api-interface/groups/join-requests/cancel.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/join-requests/cancel.ts rename to apps/client/src/code/areas/api-interface/groups/join-requests/cancel.ts diff --git a/apps/client/src/code/api-interface/groups/join-requests/reject.ts b/apps/client/src/code/areas/api-interface/groups/join-requests/reject.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/join-requests/reject.ts rename to apps/client/src/code/areas/api-interface/groups/join-requests/reject.ts diff --git a/apps/client/src/code/api-interface/groups/join-requests/send.ts b/apps/client/src/code/areas/api-interface/groups/join-requests/send.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/join-requests/send.ts rename to apps/client/src/code/areas/api-interface/groups/join-requests/send.ts diff --git a/apps/client/src/code/api-interface/groups/key-rotation.ts b/apps/client/src/code/areas/api-interface/groups/key-rotation.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/key-rotation.ts rename to apps/client/src/code/areas/api-interface/groups/key-rotation.ts diff --git a/apps/client/src/code/api-interface/groups/password/change.ts b/apps/client/src/code/areas/api-interface/groups/password/change.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/password/change.ts rename to apps/client/src/code/areas/api-interface/groups/password/change.ts diff --git a/apps/client/src/code/api-interface/groups/password/disable.ts b/apps/client/src/code/areas/api-interface/groups/password/disable.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/password/disable.ts rename to apps/client/src/code/areas/api-interface/groups/password/disable.ts diff --git a/apps/client/src/code/api-interface/groups/password/enable.ts b/apps/client/src/code/areas/api-interface/groups/password/enable.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/password/enable.ts rename to apps/client/src/code/areas/api-interface/groups/password/enable.ts diff --git a/apps/client/src/code/api-interface/groups/privacy/make-private.ts b/apps/client/src/code/areas/api-interface/groups/privacy/make-private.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/privacy/make-private.ts rename to apps/client/src/code/areas/api-interface/groups/privacy/make-private.ts diff --git a/apps/client/src/code/api-interface/groups/privacy/make-public.ts b/apps/client/src/code/areas/api-interface/groups/privacy/make-public.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/privacy/make-public.ts rename to apps/client/src/code/areas/api-interface/groups/privacy/make-public.ts diff --git a/apps/client/src/code/api-interface/groups/remove-user.ts b/apps/client/src/code/areas/api-interface/groups/remove-user.ts similarity index 100% rename from apps/client/src/code/api-interface/groups/remove-user.ts rename to apps/client/src/code/areas/api-interface/groups/remove-user.ts diff --git a/apps/client/src/code/api-interface/pages/backlinks/create.ts b/apps/client/src/code/areas/api-interface/pages/backlinks/create.ts similarity index 100% rename from apps/client/src/code/api-interface/pages/backlinks/create.ts rename to apps/client/src/code/areas/api-interface/pages/backlinks/create.ts diff --git a/apps/client/src/code/api-interface/pages/bump.ts b/apps/client/src/code/areas/api-interface/pages/bump.ts similarity index 100% rename from apps/client/src/code/api-interface/pages/bump.ts rename to apps/client/src/code/areas/api-interface/pages/bump.ts diff --git a/apps/client/src/code/api-interface/pages/create.ts b/apps/client/src/code/areas/api-interface/pages/create.ts similarity index 100% rename from apps/client/src/code/api-interface/pages/create.ts rename to apps/client/src/code/areas/api-interface/pages/create.ts diff --git a/apps/client/src/code/api-interface/pages/deletion/delete-permanently.ts b/apps/client/src/code/areas/api-interface/pages/deletion/delete-permanently.ts similarity index 100% rename from apps/client/src/code/api-interface/pages/deletion/delete-permanently.ts rename to apps/client/src/code/areas/api-interface/pages/deletion/delete-permanently.ts diff --git a/apps/client/src/code/api-interface/pages/deletion/delete.ts b/apps/client/src/code/areas/api-interface/pages/deletion/delete.ts similarity index 100% rename from apps/client/src/code/api-interface/pages/deletion/delete.ts rename to apps/client/src/code/areas/api-interface/pages/deletion/delete.ts diff --git a/apps/client/src/code/api-interface/pages/deletion/restore.ts b/apps/client/src/code/areas/api-interface/pages/deletion/restore.ts similarity index 100% rename from apps/client/src/code/api-interface/pages/deletion/restore.ts rename to apps/client/src/code/areas/api-interface/pages/deletion/restore.ts diff --git a/apps/client/src/code/api-interface/pages/move.ts b/apps/client/src/code/areas/api-interface/pages/move.ts similarity index 100% rename from apps/client/src/code/api-interface/pages/move.ts rename to apps/client/src/code/areas/api-interface/pages/move.ts diff --git a/apps/client/src/code/api-interface/pages/snapshots/delete.ts b/apps/client/src/code/areas/api-interface/pages/snapshots/delete.ts similarity index 100% rename from apps/client/src/code/api-interface/pages/snapshots/delete.ts rename to apps/client/src/code/areas/api-interface/pages/snapshots/delete.ts diff --git a/apps/client/src/code/api-interface/pages/snapshots/restore.ts b/apps/client/src/code/areas/api-interface/pages/snapshots/restore.ts similarity index 100% rename from apps/client/src/code/api-interface/pages/snapshots/restore.ts rename to apps/client/src/code/areas/api-interface/pages/snapshots/restore.ts diff --git a/apps/client/src/code/api-interface/pages/snapshots/save.ts b/apps/client/src/code/areas/api-interface/pages/snapshots/save.ts similarity index 100% rename from apps/client/src/code/api-interface/pages/snapshots/save.ts rename to apps/client/src/code/areas/api-interface/pages/snapshots/save.ts diff --git a/apps/client/src/code/api-interface/users/add-favorite-pages.ts b/apps/client/src/code/areas/api-interface/users/add-favorite-pages.ts similarity index 100% rename from apps/client/src/code/api-interface/users/add-favorite-pages.ts rename to apps/client/src/code/areas/api-interface/users/add-favorite-pages.ts diff --git a/apps/client/src/code/api-interface/users/change-email.ts b/apps/client/src/code/areas/api-interface/users/change-email.ts similarity index 100% rename from apps/client/src/code/api-interface/users/change-email.ts rename to apps/client/src/code/areas/api-interface/users/change-email.ts diff --git a/apps/client/src/code/api-interface/users/change-password.ts b/apps/client/src/code/areas/api-interface/users/change-password.ts similarity index 100% rename from apps/client/src/code/api-interface/users/change-password.ts rename to apps/client/src/code/areas/api-interface/users/change-password.ts diff --git a/apps/client/src/code/api-interface/users/delete-account.ts b/apps/client/src/code/areas/api-interface/users/delete-account.ts similarity index 100% rename from apps/client/src/code/api-interface/users/delete-account.ts rename to apps/client/src/code/areas/api-interface/users/delete-account.ts diff --git a/apps/client/src/code/api-interface/users/remove-favorite-pages.ts b/apps/client/src/code/areas/api-interface/users/remove-favorite-pages.ts similarity index 100% rename from apps/client/src/code/api-interface/users/remove-favorite-pages.ts rename to apps/client/src/code/areas/api-interface/users/remove-favorite-pages.ts diff --git a/apps/client/src/code/api-interface/users/remove-recent-pages.ts b/apps/client/src/code/areas/api-interface/users/remove-recent-pages.ts similarity index 100% rename from apps/client/src/code/api-interface/users/remove-recent-pages.ts rename to apps/client/src/code/areas/api-interface/users/remove-recent-pages.ts diff --git a/apps/client/src/code/api-interface/users/rotate-keys.ts b/apps/client/src/code/areas/api-interface/users/rotate-keys.ts similarity index 100% rename from apps/client/src/code/api-interface/users/rotate-keys.ts rename to apps/client/src/code/areas/api-interface/users/rotate-keys.ts diff --git a/apps/client/src/code/auth/demo.ts b/apps/client/src/code/areas/auth/demo.ts similarity index 88% rename from apps/client/src/code/auth/demo.ts rename to apps/client/src/code/areas/auth/demo.ts index a6f13ba8..f08f0074 100644 --- a/apps/client/src/code/auth/demo.ts +++ b/apps/client/src/code/areas/auth/demo.ts @@ -1,8 +1,8 @@ import { wrapSymmetricKey } from '@stdlib/crypto'; import sodium from 'libsodium-wrappers-sumo'; -import type { deriveUserValues } from '../crypto'; -import { trpcClient } from '../trpc'; +import type { deriveUserValues } from '../../crypto'; +import { trpcClient } from '../../trpc'; import { login } from './login'; import { getRegistrationValues } from './register'; diff --git a/apps/client/src/code/auth/login.ts b/apps/client/src/code/areas/auth/login.ts similarity index 98% rename from apps/client/src/code/auth/login.ts rename to apps/client/src/code/areas/auth/login.ts index 905950b3..000b6ca1 100644 --- a/apps/client/src/code/auth/login.ts +++ b/apps/client/src/code/areas/auth/login.ts @@ -3,7 +3,7 @@ import type { SymmetricKey } from '@stdlib/crypto'; import { createPrivateKeyring } from '@stdlib/crypto'; import { createSymmetricKeyring, wrapSymmetricKey } from '@stdlib/crypto'; -import { multiModePath } from '../utils/misc'; +import { multiModePath } from '../../utils/misc'; import { storeClientTokenExpirations } from './tokens'; export async function login(input: { diff --git a/apps/client/src/code/auth/logout.ts b/apps/client/src/code/areas/auth/logout.ts similarity index 87% rename from apps/client/src/code/auth/logout.ts rename to apps/client/src/code/areas/auth/logout.ts index 6ab83eb4..28b76b54 100644 --- a/apps/client/src/code/auth/logout.ts +++ b/apps/client/src/code/areas/auth/logout.ts @@ -1,6 +1,6 @@ -import { clearCookie } from '../cookies'; -import { trpcClient } from '../trpc'; -import { multiModePath } from '../utils/misc'; +import { clearCookie } from '../../cookies'; +import { trpcClient } from '../../trpc'; +import { multiModePath } from '../../utils/misc'; import { clearClientTokenExpirations } from './tokens'; export async function logout() { diff --git a/apps/client/src/code/auth/refresh.ts b/apps/client/src/code/areas/auth/refresh.ts similarity index 97% rename from apps/client/src/code/auth/refresh.ts rename to apps/client/src/code/areas/auth/refresh.ts index 929aaa71..dde6a31a 100644 --- a/apps/client/src/code/auth/refresh.ts +++ b/apps/client/src/code/areas/auth/refresh.ts @@ -12,8 +12,8 @@ import { wrapSymmetricKey, } from '@stdlib/crypto'; -import { redirectIfNecessary } from '../routing'; -import { trpcClient } from '../trpc'; +import { redirectIfNecessary } from '../../routing'; +import { trpcClient } from '../../trpc'; import { logout } from './logout'; import { areClientTokensExpiring, diff --git a/apps/client/src/code/auth/register.ts b/apps/client/src/code/areas/auth/register.ts similarity index 100% rename from apps/client/src/code/auth/register.ts rename to apps/client/src/code/areas/auth/register.ts diff --git a/apps/client/src/code/auth/tokens.ts b/apps/client/src/code/areas/auth/tokens.ts similarity index 97% rename from apps/client/src/code/auth/tokens.ts rename to apps/client/src/code/areas/auth/tokens.ts index a9b4dde5..cf9ac06f 100644 --- a/apps/client/src/code/auth/tokens.ts +++ b/apps/client/src/code/areas/auth/tokens.ts @@ -3,7 +3,7 @@ import { getRefreshTokenExpiration, } from '@deeplib/misc'; -import { shouldRememberSession } from '../utils/misc'; +import { shouldRememberSession } from '../../utils/misc'; export function getClientTokenExpirationDate( token: 'access' | 'refresh', diff --git a/apps/client/src/code/realtime/client.ts b/apps/client/src/code/areas/realtime/client.ts similarity index 99% rename from apps/client/src/code/realtime/client.ts rename to apps/client/src/code/areas/realtime/client.ts index 94ef71de..0342c747 100644 --- a/apps/client/src/code/realtime/client.ts +++ b/apps/client/src/code/areas/realtime/client.ts @@ -13,7 +13,7 @@ import * as encoding from 'lib0/encoding'; import { once, throttle } from 'lodash'; import { pack, unpack } from 'msgpackr'; -import { getNotificationInfo } from '../pages/notifications/notifications'; +import { getNotificationInfo } from '../../pages/notifications/notifications'; import { RealtimeContext } from './context'; export interface RealtimeCommand { diff --git a/apps/client/src/code/realtime/context.ts b/apps/client/src/code/areas/realtime/context.ts similarity index 100% rename from apps/client/src/code/realtime/context.ts rename to apps/client/src/code/areas/realtime/context.ts diff --git a/apps/client/src/code/tiptap/image-resize/NodeView.vue b/apps/client/src/code/areas/tiptap/image-resize/NodeView.vue similarity index 100% rename from apps/client/src/code/tiptap/image-resize/NodeView.vue rename to apps/client/src/code/areas/tiptap/image-resize/NodeView.vue diff --git a/apps/client/src/code/tiptap/image-resize/extension.ts b/apps/client/src/code/areas/tiptap/image-resize/extension.ts similarity index 100% rename from apps/client/src/code/tiptap/image-resize/extension.ts rename to apps/client/src/code/areas/tiptap/image-resize/extension.ts diff --git a/apps/client/src/code/tiptap/inline-math/NodeView.vue b/apps/client/src/code/areas/tiptap/inline-math/NodeView.vue similarity index 100% rename from apps/client/src/code/tiptap/inline-math/NodeView.vue rename to apps/client/src/code/areas/tiptap/inline-math/NodeView.vue diff --git a/apps/client/src/code/tiptap/inline-math/extension.ts b/apps/client/src/code/areas/tiptap/inline-math/extension.ts similarity index 100% rename from apps/client/src/code/tiptap/inline-math/extension.ts rename to apps/client/src/code/areas/tiptap/inline-math/extension.ts diff --git a/apps/client/src/code/tiptap/math-block/NodeView.vue b/apps/client/src/code/areas/tiptap/math-block/NodeView.vue similarity index 100% rename from apps/client/src/code/tiptap/math-block/NodeView.vue rename to apps/client/src/code/areas/tiptap/math-block/NodeView.vue diff --git a/apps/client/src/code/tiptap/math-block/extension.ts b/apps/client/src/code/areas/tiptap/math-block/extension.ts similarity index 100% rename from apps/client/src/code/tiptap/math-block/extension.ts rename to apps/client/src/code/areas/tiptap/math-block/extension.ts diff --git a/apps/client/src/code/tiptap/paste-handler.ts b/apps/client/src/code/areas/tiptap/paste-handler.ts similarity index 100% rename from apps/client/src/code/tiptap/paste-handler.ts rename to apps/client/src/code/areas/tiptap/paste-handler.ts diff --git a/apps/client/src/code/tiptap/task-item.ts b/apps/client/src/code/areas/tiptap/task-item.ts similarity index 100% rename from apps/client/src/code/tiptap/task-item.ts rename to apps/client/src/code/areas/tiptap/task-item.ts diff --git a/apps/client/src/code/tiptap/utils.ts b/apps/client/src/code/areas/tiptap/utils.ts similarity index 100% rename from apps/client/src/code/tiptap/utils.ts rename to apps/client/src/code/areas/tiptap/utils.ts diff --git a/apps/client/src/code/tiptap/youtube-video/NodeView.vue b/apps/client/src/code/areas/tiptap/youtube-video/NodeView.vue similarity index 100% rename from apps/client/src/code/tiptap/youtube-video/NodeView.vue rename to apps/client/src/code/areas/tiptap/youtube-video/NodeView.vue diff --git a/apps/client/src/code/tiptap/youtube-video/extension.ts b/apps/client/src/code/areas/tiptap/youtube-video/extension.ts similarity index 100% rename from apps/client/src/code/tiptap/youtube-video/extension.ts rename to apps/client/src/code/areas/tiptap/youtube-video/extension.ts diff --git a/apps/client/src/code/tiptap/youtube-video/utils.ts b/apps/client/src/code/areas/tiptap/youtube-video/utils.ts similarity index 100% rename from apps/client/src/code/tiptap/youtube-video/utils.ts rename to apps/client/src/code/areas/tiptap/youtube-video/utils.ts diff --git a/apps/client/src/code/pages/composables/use-keyboard-shortcuts.ts b/apps/client/src/code/pages/composables/use-keyboard-shortcuts.ts index 0f553d76..843b8135 100644 --- a/apps/client/src/code/pages/composables/use-keyboard-shortcuts.ts +++ b/apps/client/src/code/pages/composables/use-keyboard-shortcuts.ts @@ -1,6 +1,6 @@ import { Vec2 } from '@stdlib/misc'; import { useEventListener } from '@vueuse/core'; -import { unsetNode } from 'src/code/tiptap/utils'; +import { unsetNode } from 'src/code/areas/tiptap/utils'; import { modsMatch } from 'src/code/utils/misc'; import InsertImageDialog from 'src/layouts/PagesLayout/MainToolbar/InsertImageDialog.vue'; import InsertLinkDialog from 'src/layouts/PagesLayout/MainToolbar/InsertLinkDialog.vue'; diff --git a/apps/client/src/code/pages/composables/use-page-navigation-interception.ts b/apps/client/src/code/pages/composables/use-page-navigation-interception.ts index d97dc628..46338fd5 100644 --- a/apps/client/src/code/pages/composables/use-page-navigation-interception.ts +++ b/apps/client/src/code/pages/composables/use-page-navigation-interception.ts @@ -1,6 +1,6 @@ import { useEventListener } from '@vueuse/core'; -import { imageResizing } from 'src/code/tiptap/image-resize/NodeView.vue'; -import { youtubeResizing } from 'src/code/tiptap/youtube-video/NodeView.vue'; +import { imageResizing } from 'src/code/areas/tiptap/image-resize/NodeView.vue'; +import { youtubeResizing } from 'src/code/areas/tiptap/youtube-video/NodeView.vue'; import { handleError, isCtrlDown } from 'src/code/utils/misc'; export function usePageNavigationInterception() { diff --git a/apps/client/src/code/pages/notifications/group-invitation-sent.ts b/apps/client/src/code/pages/notifications/group-invitation-sent.ts index a2204c9c..b17d0030 100644 --- a/apps/client/src/code/pages/notifications/group-invitation-sent.ts +++ b/apps/client/src/code/pages/notifications/group-invitation-sent.ts @@ -1,4 +1,4 @@ -import { rejectJoinInvitation } from 'src/code/api-interface/groups/join-invitations/reject'; +import { rejectJoinInvitation } from 'src/code/areas/api-interface/groups/join-invitations/reject'; import { asyncDialog, handleError } from 'src/code/utils/misc'; import AcceptInvitationDialog from 'src/layouts/PagesLayout/MainContent/DisplayPage/DisplayScreens/AcceptInvitationDialog.vue'; diff --git a/apps/client/src/code/pages/notifications/group-request-sent.ts b/apps/client/src/code/pages/notifications/group-request-sent.ts index 2c54ea9a..dc41d8cd 100644 --- a/apps/client/src/code/pages/notifications/group-request-sent.ts +++ b/apps/client/src/code/pages/notifications/group-request-sent.ts @@ -1,4 +1,4 @@ -import { rejectJoinRequest } from 'src/code/api-interface/groups/join-requests/reject'; +import { rejectJoinRequest } from 'src/code/areas/api-interface/groups/join-requests/reject'; import { asyncDialog, handleError } from 'src/code/utils/misc'; import GroupSettingsDialog from 'src/layouts/PagesLayout/RightSidebar/PageProperties/GroupSettingsDialog/GroupSettingsDialog.vue'; import AcceptRequestDialog from 'src/layouts/PagesLayout/RightSidebar/PageProperties/GroupSettingsDialog/RequestsTab/AcceptRequestDialog.vue'; diff --git a/apps/client/src/code/pages/page/page.ts b/apps/client/src/code/pages/page/page.ts index 43055d6c..8928110c 100644 --- a/apps/client/src/code/pages/page/page.ts +++ b/apps/client/src/code/pages/page/page.ts @@ -2,10 +2,10 @@ import { rolesMap } from '@deeplib/misc'; import { isNanoID, sleep, Vec2 } from '@stdlib/misc'; import { watchUntilTrue } from '@stdlib/vue'; import { once, pull } from 'lodash'; -import { bumpPage } from 'src/code/api-interface/pages/bump'; +import { bumpPage } from 'src/code/areas/api-interface/pages/bump'; +import { RealtimeContext } from 'src/code/areas/realtime/context'; import type { Factories } from 'src/code/factories'; import type { Pages } from 'src/code/pages/pages'; -import { RealtimeContext } from 'src/code/realtime/context'; import { scrollIntoView } from 'src/code/utils/scroll-into-view'; import type { ComputedRef, UnwrapNestedRefs } from 'vue'; import type { z } from 'zod'; diff --git a/apps/client/src/code/pages/page/selection/selection.ts b/apps/client/src/code/pages/page/selection/selection.ts index 9e48a822..01760e84 100644 --- a/apps/client/src/code/pages/page/selection/selection.ts +++ b/apps/client/src/code/pages/page/selection/selection.ts @@ -2,7 +2,7 @@ import type { MarkName, NodeName } from '@stdlib/misc'; import { Vec2 } from '@stdlib/misc'; import type { ChainedCommands, Editor } from '@tiptap/vue-3'; import { every } from 'lodash'; -import { unsetNode } from 'src/code/tiptap/utils'; +import { unsetNode } from 'src/code/areas/tiptap/utils'; import { getClipboardText, setClipboardText } from 'src/code/utils/clipboard'; import type { ComputedRef, UnwrapNestedRefs } from 'vue'; diff --git a/apps/client/src/components/LinkURL.vue b/apps/client/src/components/LinkURL.vue index d8f82967..2628ebb7 100644 --- a/apps/client/src/components/LinkURL.vue +++ b/apps/client/src/components/LinkURL.vue @@ -17,7 +17,7 @@ + + +
+ + + diff --git a/new-deepnotes/apps/marketing/package-lock.json b/new-deepnotes/apps/marketing/package-lock.json new file mode 100644 index 00000000..2d0420c2 --- /dev/null +++ b/new-deepnotes/apps/marketing/package-lock.json @@ -0,0 +1,8056 @@ +{ + "name": "@deepnotes/marketing", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@deepnotes/marketing", + "version": "0.0.0", + "dependencies": { + "@tailwindcss/vite": "^4.2.4", + "@vueuse/core": "^14.3.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "marked": "^18.0.4", + "reka-ui": "^2.9.8", + "shadcn-vue": "^2.6.2", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.2.4", + "tw-animate-css": "^1.4.0", + "vue": "^3.5.13", + "vue-router": "4" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.19", + "@unhead/vue": "^2.0.19", + "@vitejs/plugin-vue": "^5.2.3", + "typescript": "^5.8.3", + "vite": "^6.3.3", + "vite-ssg": "^28.2.2", + "vue-tsc": "^2.2.10" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", + "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", + "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@dotenvx/dotenvx": { + "version": "1.69.2", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.69.2.tgz", + "integrity": "sha512-60Vh31k5bAAPq57Isx+Iopl2FrZQzZyUc4kh/OESgXiwhrKXqdxEn3JGTGSawng1DkkeC4jofFq1VpPJqNQx4w==", + "license": "BSD-3-Clause", + "dependencies": { + "commander": "^11.1.0", + "dotenv": "^17.2.1", + "eciesjs": "^0.4.10", + "enquirer": "^2.4.1", + "execa": "^5.1.1", + "fdir": "^6.2.0", + "ignore": "^5.3.0", + "object-treeify": "1.1.33", + "picomatch": "^4.0.4", + "which": "^4.0.0", + "yocto-spinner": "^1.1.0" + }, + "bin": { + "dotenvx": "src/cli/dotenvx.js" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/@ecies/ciphers": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.6.tgz", + "integrity": "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==", + "license": "MIT", + "engines": { + "bun": ">=1", + "deno": ">=2.7.10", + "node": ">=16" + }, + "peerDependencies": { + "@noble/ciphers": "^1.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@floating-ui/vue": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.11.tgz", + "integrity": "sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6", + "@floating-ui/utils": "^0.2.11", + "vue-demi": ">=0.13.0" + } + }, + "node_modules/@floating-ui/vue/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@internationalized/date": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.2.tgz", + "integrity": "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/number": { + "version": "3.6.7", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.7.tgz", + "integrity": "sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.16.0.tgz", + "integrity": "sha512-Er2N7q3WOiH6y2JLxsxNX+u2/sLqSsL0bxFgDjuiPiA7vKhZRm+IzcS17vRee3GNXr64UsesA5CAp9yTiIYw9A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/vue-virtual": { + "version": "3.13.26", + "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.26.tgz", + "integrity": "sha512-4TmREKi8rKiQC8E2XVEMMgzWbrgHNYolkBgYTXVK1kqXmXRGz6xPWgBq20GUYWUDDhit94+g0ricUQKpZhWRmg==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.16.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.0.0" + } + }, + "node_modules/@ts-morph/common": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.28.1.tgz", + "integrity": "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==", + "license": "MIT", + "dependencies": { + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1", + "tinyglobby": "^0.2.14" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@unhead/dom": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-2.1.15.tgz", + "integrity": "sha512-3/qtu2uOVW0eyYCIljweQ9sRDiFpCasGv9A7LjbcqWR9cxEPDYiJBiK2lVqPJT68qonAeXhtiS08PTCWTau8SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + }, + "peerDependencies": { + "unhead": "2.1.15" + } + }, + "node_modules/@unhead/vue": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.1.15.tgz", + "integrity": "sha512-SSByXfEjhzPn8gXdEdgpYqpLMPSkLUH2HVE0GxZfOtNsJ0GgOHQs0g9T67ZZ1z0kTELLKdtOtYrzrbv9+ffF7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookable": "^6.0.1", + "unhead": "2.1.15" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + }, + "peerDependencies": { + "vue": ">=3.5.18" + } + }, + "node_modules/@unovue/detypes": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@unovue/detypes/-/detypes-0.8.5.tgz", + "integrity": "sha512-Yz4JeWOHGa+w/3YudVdng8hgN/VGW9cvp8xmFkmPPFzalGblLPPSpIRiwVo853yLstMZO2LLwe0vOoLAQsUQXw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@vue/compiler-dom": "^3.4.27", + "@vue/compiler-sfc": "^3.4.27", + "@vuedx/template-ast-types": "0.7.1", + "fast-glob": "^3.3.2", + "prettier": "^3.2.5", + "typescript": "^5.4.5" + }, + "bin": { + "detypes": "detype.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.35.tgz", + "integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.35", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz", + "integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.35", + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz", + "integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.35", + "@vue/compiler-dom": "3.5.35", + "@vue/compiler-ssr": "3.5.35", + "@vue/shared": "3.5.35", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz", + "integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.35", + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/language-core/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vue/language-core/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.35.tgz", + "integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.35.tgz", + "integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.35", + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz", + "integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.35", + "@vue/runtime-core": "3.5.35", + "@vue/shared": "3.5.35", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.35.tgz", + "integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.35", + "@vue/shared": "3.5.35" + }, + "peerDependencies": { + "vue": "3.5.35" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.35.tgz", + "integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==", + "license": "MIT" + }, + "node_modules/@vuedx/template-ast-types": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@vuedx/template-ast-types/-/template-ast-types-0.7.1.tgz", + "integrity": "sha512-Mqugk/F0lFN2u9bhimH6G1kSu2hhLi2WoqgCVxrMvgxm2kDc30DtdvVGRq+UgEmKVP61OudcMtZqkUoGQeFBUQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "^3.0.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/znck" + } + }, + "node_modules/@vueuse/core": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.1.tgz", + "integrity": "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ast-types": { + "name": "ast-types-x", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/ast-types-x/-/ast-types-x-1.18.0.tgz", + "integrity": "sha512-ZtfIlyTCmnAXPCQo4mSDtFsHL7L3q0sJfpVYPmy5uYPjs+fynzOuc1Cg6yQ9fF6h61RjEWtOlRFwV1Kc80Qs6A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-types-x": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/ast-types-x/-/ast-types-x-1.18.0.tgz", + "integrity": "sha512-ZtfIlyTCmnAXPCQo4mSDtFsHL7L3q0sJfpVYPmy5uYPjs+fynzOuc1Cg6yQ9fF6h61RjEWtOlRFwV1Kc80Qs6A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.33", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", + "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "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", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-progress/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-hrtime": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", + "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", + "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-diff": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz", + "integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eciesjs": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.18.tgz", + "integrity": "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==", + "license": "MIT", + "dependencies": { + "@ecies/ciphers": "^0.2.5", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0" + }, + "engines": { + "bun": ">=1", + "deno": ">=2", + "node": ">=16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.364", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", + "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.22.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz", + "integrity": "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz", + "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-timeout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-1.0.2.tgz", + "integrity": "sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fuzzysort": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", + "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", + "license": "MIT" + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz", + "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz", + "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==", + "license": "MIT", + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gonzales-pe": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz", + "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "gonzales": "bin/gonzales.js" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hono": { + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", + "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hookable": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz", + "integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-minifier-terser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "~5.3.2", + "commander": "^10.0.0", + "entities": "^4.4.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.15.1" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/html-minifier-terser/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/html5parser": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html5parser/-/html5parser-2.0.2.tgz", + "integrity": "sha512-L0y+IdTVxHsovmye8MBtFgBvWZnq1C9WnI/SmJszxoQjmUH1psX2uzDk21O5k5et6udxdGjwxkbmT9eVRoG05w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.2.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/identifier-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/identifier-regex/-/identifier-regex-1.0.1.tgz", + "integrity": "sha512-ZrYyM0sozNPZlvBvE7Oq9Bn44n0qKGrYu5sQ0JzMUnjIhpgWYE2JB6aBoFwEYdPjqj7jPyxXTMJiHDOxDfd8yw==", + "license": "MIT", + "dependencies": { + "reserved-identifiers": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-identifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-identifier/-/is-identifier-1.0.1.tgz", + "integrity": "sha512-HQ5v4rEJ7REUV54bCd2l5FaD299SGDEn2UPoVXaTHAyGviLq2menVUD2udi3trQ32uvB6LdAh/0ck2EuizrtpA==", + "license": "MIT", + "dependencies": { + "identifier-regex": "^1.0.0", + "super-regex": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", + "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^6.0.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "undici": "^7.21.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash.sortedlastindex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.sortedlastindex/-/lodash.sortedlastindex-4.1.0.tgz", + "integrity": "sha512-s8xEQdsp2Tu5zUqVdFSe9C0kR8YlnAJYLqMdkh+pIRBRxF6/apWseLdHl3/+jv2I61dhPwtI/Ff+EqvCpc+N8w==", + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-asynchronous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/make-asynchronous/-/make-asynchronous-1.1.0.tgz", + "integrity": "sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==", + "license": "MIT", + "dependencies": { + "p-event": "^6.0.0", + "type-fest": "^4.6.0", + "web-worker": "^1.5.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marked": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.4.tgz", + "integrity": "sha512-c/BTaKzg0G6ezQx97DAkYU7k0HM6ys0FqYeKBL6hlBByZwy+ycA1+f0vDdjMHKKeEjdgkx0GOv9Il6D+85cOqA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-html-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.1.0.tgz", + "integrity": "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nypm": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.6.tgz", + "integrity": "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==", + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.1.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-treeify": { + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", + "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.0.tgz", + "integrity": "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.2", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-event": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", + "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", + "license": "MIT", + "dependencies": { + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-less": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-6.0.0.tgz", + "integrity": "sha512-FPX16mQLyEjLzEuuJtxA8X3ejDLNGGEG503d2YGZR5Ask1SpDN8KmZUMpzCvyalWRywAn1n1VOA5dcqfCLo5rg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "postcss": "^8.3.5" + } + }, + "node_modules/postcss-sass": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.5.0.tgz", + "integrity": "sha512-qtu8awh1NMF3o9j/x9j3EZnd+BlF66X6NZYl12BdKoG2Z4hmydOt/dZj2Nq+g0kfk2pQy3jeYFBmvG9DBwynGQ==", + "license": "MIT", + "dependencies": { + "gonzales-pe": "^4.3.0", + "postcss": "^8.2.14" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-styl": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/postcss-styl/-/postcss-styl-0.12.3.tgz", + "integrity": "sha512-8I7Cd8sxiEITIp32xBK4K/Aj1ukX6vuWnx8oY/oAH35NfQI4OZaY5nd68Yx8HeN5S49uhQ6DL0rNk0ZBu/TaLg==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fast-diff": "^1.2.0", + "lodash.sortedlastindex": "^4.1.0", + "postcss": "^7.0.27 || ^8.0.0", + "stylus": "^0.57.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || ^11.10.1 || >=12.13.0" + }, + "funding": { + "url": "https://opencollective.com/stylus" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc9": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", + "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.6", + "destr": "^2.0.5" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recast-x": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/recast-x/-/recast-x-1.0.5.tgz", + "integrity": "sha512-CkfWKhQiYsMQYaWUkHdERXUxT2jJLBoa5y7zFv3dUAE7Ly5oU/0hsqrENyEfrCL03pDsQYbnoz17Cbagx/c2OA==", + "license": "MIT", + "dependencies": { + "ast-types": "npm:ast-types-x@1.18.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reka-ui": { + "version": "2.9.8", + "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.9.8.tgz", + "integrity": "sha512-7dxaBJ6nQ0zOQZXPV45219tTEgZPstmihBLS9ABPhSiPiJ8SiF0sacfZHFaBptS0v9N4tzsevq+8MNBpE4p5JQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.6.13", + "@floating-ui/vue": "^1.1.6", + "@internationalized/date": "^3.5.0", + "@internationalized/number": "^3.5.0", + "@tanstack/vue-virtual": "^3.12.0", + "@vueuse/core": "^14.1.0", + "@vueuse/shared": "^14.1.0", + "aria-hidden": "^1.2.4", + "defu": "^6.1.5", + "ohash": "^2.0.11" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/zernonia" + }, + "peerDependencies": { + "vue": ">= 3.4.0" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reserved-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz", + "integrity": "sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "license": "ISC" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shadcn-vue": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/shadcn-vue/-/shadcn-vue-2.7.3.tgz", + "integrity": "sha512-bYfn9RbjG98++IjvCEhVhvva64mjDAGrtE4QNSuy6jjtE/XpI3syRJaBYmrvPBI93W27dLGbCTr8p7gnvjkQvQ==", + "license": "MIT", + "dependencies": { + "@dotenvx/dotenvx": "^1.51.1", + "@modelcontextprotocol/sdk": "^1.24.3", + "@unovue/detypes": "^0.8.5", + "@vue/compiler-sfc": "^3.5", + "c12": "^3.3.2", + "commander": "^14.0.2", + "consola": "^3.4.2", + "dedent": "^1.7.0", + "deepmerge": "^4.3.1", + "diff": "^8.0.2", + "fs-extra": "^11.3.2", + "fuzzysort": "^3.1.0", + "get-tsconfig": "^4.13.0", + "giget": "^3.2.0", + "magic-string": "^0.30.21", + "nypm": "^0.6.2", + "ofetch": "^1.5.1", + "open": "^10.2.0", + "ora": "^9.0.0", + "pathe": "^2.0.3", + "postcss": "^8.5.10", + "postcss-selector-parser": "^7.1.1", + "prompts": "^2.4.2", + "reka-ui": "^2.9.2", + "semver": "^7.7.3", + "stringify-object": "^6.0.0", + "tailwindcss": "^4.1.17", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "ts-morph": "^27.0.2", + "undici": "^7.16.0", + "validate-npm-package-name": "^5.0.1", + "vue-metamorph": "3.3.4", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.0" + }, + "bin": { + "shadcn-vue": "dist/index.js" + } + }, + "node_modules/shadcn-vue/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "license": "MIT", + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/stringify-object": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-6.0.0.tgz", + "integrity": "sha512-6f94vIED6vmJJfh3lyVsVWxCYSfI5uM+16ntED/Ql37XIyV6kj0mRAAiTeMMc/QLYIaizC3bUprQ8pQnDDrKfA==", + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-keys": "^1.0.0", + "is-identifier": "^1.0.1", + "is-obj": "^3.0.0", + "is-regexp": "^3.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/stylus": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.57.0.tgz", + "integrity": "sha512-yOI6G8WYfr0q8v8rRvE91wbxFU+rJPo760Va4MF6K0I6BZjO4r+xSynkvyPBP9tV1CIEUeRsiidjIs2rzb1CnQ==", + "license": "MIT", + "dependencies": { + "css": "^3.0.0", + "debug": "^4.3.2", + "glob": "^7.1.6", + "safer-buffer": "^2.1.2", + "sax": "~1.2.4", + "source-map": "^0.7.3" + }, + "bin": { + "stylus": "bin/stylus" + }, + "engines": { + "node": "*" + } + }, + "node_modules/stylus/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/stylus/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/stylus/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stylus/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/stylus/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/super-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-1.1.0.tgz", + "integrity": "sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==", + "license": "MIT", + "dependencies": { + "function-timeout": "^1.0.1", + "make-asynchronous": "^1.0.1", + "time-span": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "license": "MIT", + "peer": true + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "devOptional": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/time-span": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", + "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", + "license": "MIT", + "dependencies": { + "convert-hrtime": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tldts": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz", + "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.2" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz", + "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-morph": { + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-27.0.2.tgz", + "integrity": "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==", + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.28.1", + "code-block-writer": "^13.0.3" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.26.0.tgz", + "integrity": "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unhead": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.15.tgz", + "integrity": "sha512-MCt5T90mCWyr3Z6pUCdM9lVRXoMoVBlL7z7U4CYVIiaDiuzad/UCfLuMqz5MeNmpZUgoBCQnrucJimU7EZR+XA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hookable": "^6.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-ssg": { + "version": "28.3.0", + "resolved": "https://registry.npmjs.org/vite-ssg/-/vite-ssg-28.3.0.tgz", + "integrity": "sha512-dIUjv+scfhJTfYGwf83R0KGAmr/duIo9oln5ZsQOIZZACm3voAON//7oKhtEwaOTtD4QTTab+nhvyn0QiIaFMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unhead/dom": "^2.1.2", + "@unhead/vue": "^2.1.2", + "ansis": "^4.2.0", + "cac": "^6.7.14", + "html-minifier-terser": "^7.2.0", + "html5parser": "^2.0.2", + "jsdom": "^28.0.0" + }, + "bin": { + "vite-ssg": "bin/vite-ssg.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "beasties": "^0.3.5", + "prettier": "^3.3.0", + "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0-0 || ^8.0.0-0", + "vue": "^3.2.10", + "vue-router": "^4.0.1 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "beasties": { + "optional": true + }, + "prettier": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz", + "integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.35", + "@vue/compiler-sfc": "3.5.35", + "@vue/runtime-dom": "3.5.35", + "@vue/server-renderer": "3.5.35", + "@vue/shared": "3.5.35" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz", + "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/vue-metamorph": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/vue-metamorph/-/vue-metamorph-3.3.4.tgz", + "integrity": "sha512-WZ1xzHrmYh9UiZ7OC9eG1ASzgSybEB10jhop+k5KzMY9I1JmRKdreqUYzbV3hOnOMvLhyDn7y6f62mLE2jHFSg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "8.0.0-alpha.12", + "ast-types-x": "1.18.0", + "chalk": "^5.3.0", + "cli-progress": "^3.12.0", + "commander": "^14.0.0", + "deep-diff": "^1.0.2", + "fs-extra": "^11.2.0", + "glob": "^11.0.0", + "lodash-es": "^4.17.21", + "magic-string": "^0.30.10", + "micromatch": "^4.0.8", + "node-html-parser": "^7.0.1", + "postcss": "^8.4.38", + "postcss-less": "^6.0.0", + "postcss-sass": "^0.5.0", + "postcss-scss": "^4.0.9", + "postcss-styl": "^0.12.3", + "recast-x": "1.0.5", + "table": "^6.8.2", + "vue-eslint-parser": "^10.1.0" + }, + "bin": { + "vue-metamorph": "scripts/scaffold.js" + } + }, + "node_modules/vue-metamorph/node_modules/@babel/parser": { + "version": "8.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-alpha.12.tgz", + "integrity": "sha512-AzWmrp4uJ+DcXVH0uoUpJVhRqxNirC0BbXsZ82AQuVod41CoaV5G+cwcvtYusrIIxv7BIJb6ce0dQ9L0wAl1iA==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": "^18.20.0 || ^20.10.0 || >=21.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yocto-spinner": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-1.2.0.tgz", + "integrity": "sha512-Yw0hUB6UA3o4YUgKy3oSe9a4cxoaZ9sBfYDw+JSxo6Id0KoJGoxzPA24qqUXYKBWABs/zDSGTz9kww7t3F0XGw==", + "license": "MIT", + "dependencies": { + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18.19" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/new-deepnotes/apps/marketing/package.json b/new-deepnotes/apps/marketing/package.json new file mode 100644 index 00000000..0d1ff995 --- /dev/null +++ b/new-deepnotes/apps/marketing/package.json @@ -0,0 +1,36 @@ +{ + "name": "@deepnotes/marketing", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite-ssg build", + "dev": "vite", + "lint": "eslint vite.config.ts \"src/**/*.ts\"", + "test": "node -e \"process.exit(0)\"", + "typecheck": "vue-tsc --noEmit -p tsconfig.json" + }, + "dependencies": { + "@tailwindcss/vite": "^4.2.4", + "@vueuse/core": "^14.3.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "marked": "^18.0.4", + "reka-ui": "^2.9.8", + "shadcn-vue": "^2.6.2", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.2.4", + "tw-animate-css": "^1.4.0", + "vue": "^3.5.13", + "vue-router": "4" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.19", + "@unhead/vue": "^2.0.19", + "@vitejs/plugin-vue": "^5.2.3", + "typescript": "^5.8.3", + "vite": "^6.3.3", + "vite-ssg": "^28.2.2", + "vue-tsc": "^2.2.10" + } +} diff --git a/new-deepnotes/apps/marketing/public/applications/biology-study-thumbnail.webp b/new-deepnotes/apps/marketing/public/applications/biology-study-thumbnail.webp new file mode 100644 index 00000000..13c1e563 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/applications/biology-study-thumbnail.webp differ diff --git a/new-deepnotes/apps/marketing/public/applications/cheat-sheet-thumbnail.webp b/new-deepnotes/apps/marketing/public/applications/cheat-sheet-thumbnail.webp new file mode 100644 index 00000000..d26b6543 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/applications/cheat-sheet-thumbnail.webp differ diff --git a/new-deepnotes/apps/marketing/public/applications/chemistry-study-thumbnail.webp b/new-deepnotes/apps/marketing/public/applications/chemistry-study-thumbnail.webp new file mode 100644 index 00000000..6c4711e2 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/applications/chemistry-study-thumbnail.webp differ diff --git a/new-deepnotes/apps/marketing/public/applications/database-structure-thumbnail.webp b/new-deepnotes/apps/marketing/public/applications/database-structure-thumbnail.webp new file mode 100644 index 00000000..13468201 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/applications/database-structure-thumbnail.webp differ diff --git a/new-deepnotes/apps/marketing/public/applications/diagram-thumbnail.webp b/new-deepnotes/apps/marketing/public/applications/diagram-thumbnail.webp new file mode 100644 index 00000000..5f825247 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/applications/diagram-thumbnail.webp differ diff --git a/new-deepnotes/apps/marketing/public/applications/family-tree-thumbnail.webp b/new-deepnotes/apps/marketing/public/applications/family-tree-thumbnail.webp new file mode 100644 index 00000000..3cbe0ad1 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/applications/family-tree-thumbnail.webp differ diff --git a/new-deepnotes/apps/marketing/public/applications/flashcards-thumbnail.webp b/new-deepnotes/apps/marketing/public/applications/flashcards-thumbnail.webp new file mode 100644 index 00000000..7aa02d84 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/applications/flashcards-thumbnail.webp differ diff --git a/new-deepnotes/apps/marketing/public/applications/history-study-thumbnail.webp b/new-deepnotes/apps/marketing/public/applications/history-study-thumbnail.webp new file mode 100644 index 00000000..bd607caa Binary files /dev/null and b/new-deepnotes/apps/marketing/public/applications/history-study-thumbnail.webp differ diff --git a/new-deepnotes/apps/marketing/public/applications/kanban-board-thumbnail.webp b/new-deepnotes/apps/marketing/public/applications/kanban-board-thumbnail.webp new file mode 100644 index 00000000..b692ddf2 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/applications/kanban-board-thumbnail.webp differ diff --git a/new-deepnotes/apps/marketing/public/applications/link-gallery-thumbnail.webp b/new-deepnotes/apps/marketing/public/applications/link-gallery-thumbnail.webp new file mode 100644 index 00000000..da962470 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/applications/link-gallery-thumbnail.webp differ diff --git a/new-deepnotes/apps/marketing/public/applications/mind-map.webp b/new-deepnotes/apps/marketing/public/applications/mind-map.webp new file mode 100644 index 00000000..b279a402 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/applications/mind-map.webp differ diff --git a/new-deepnotes/apps/marketing/public/applications/philosophy-study-thumbnail.webp b/new-deepnotes/apps/marketing/public/applications/philosophy-study-thumbnail.webp new file mode 100644 index 00000000..43ca3173 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/applications/philosophy-study-thumbnail.webp differ diff --git a/new-deepnotes/apps/marketing/public/applications/physics-study-thumbnail.webp b/new-deepnotes/apps/marketing/public/applications/physics-study-thumbnail.webp new file mode 100644 index 00000000..371f25c5 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/applications/physics-study-thumbnail.webp differ diff --git a/new-deepnotes/apps/marketing/public/black-logo.png b/new-deepnotes/apps/marketing/public/black-logo.png new file mode 100644 index 00000000..8166ea5b Binary files /dev/null and b/new-deepnotes/apps/marketing/public/black-logo.png differ diff --git a/new-deepnotes/apps/marketing/public/favicon.ico b/new-deepnotes/apps/marketing/public/favicon.ico new file mode 100644 index 00000000..cee14563 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/favicon.ico differ diff --git a/new-deepnotes/apps/marketing/public/white-logo-outline.webp b/new-deepnotes/apps/marketing/public/white-logo-outline.webp new file mode 100644 index 00000000..a5cb4ed5 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/white-logo-outline.webp differ diff --git a/new-deepnotes/apps/marketing/public/whitepaper/authentication.webp b/new-deepnotes/apps/marketing/public/whitepaper/authentication.webp new file mode 100644 index 00000000..ea302df6 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/whitepaper/authentication.webp differ diff --git a/new-deepnotes/apps/marketing/public/whitepaper/key-hierarchy.webp b/new-deepnotes/apps/marketing/public/whitepaper/key-hierarchy.webp new file mode 100644 index 00000000..70d0e677 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/whitepaper/key-hierarchy.webp differ diff --git a/new-deepnotes/apps/marketing/public/whitepaper/registration.webp b/new-deepnotes/apps/marketing/public/whitepaper/registration.webp new file mode 100644 index 00000000..6b42e4a5 Binary files /dev/null and b/new-deepnotes/apps/marketing/public/whitepaper/registration.webp differ diff --git a/new-deepnotes/apps/marketing/public/whitepaper/session-refresh.webp b/new-deepnotes/apps/marketing/public/whitepaper/session-refresh.webp new file mode 100644 index 00000000..ace7a27f Binary files /dev/null and b/new-deepnotes/apps/marketing/public/whitepaper/session-refresh.webp differ diff --git a/new-deepnotes/apps/marketing/src/App.vue b/new-deepnotes/apps/marketing/src/App.vue new file mode 100644 index 00000000..b0fa45bb --- /dev/null +++ b/new-deepnotes/apps/marketing/src/App.vue @@ -0,0 +1,7 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/components/DocumentIndexLayout.vue b/new-deepnotes/apps/marketing/src/components/DocumentIndexLayout.vue new file mode 100644 index 00000000..866631ed --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/DocumentIndexLayout.vue @@ -0,0 +1,138 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/components/Footer.vue b/new-deepnotes/apps/marketing/src/components/Footer.vue new file mode 100644 index 00000000..221d9640 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/Footer.vue @@ -0,0 +1,86 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/components/NavBar.vue b/new-deepnotes/apps/marketing/src/components/NavBar.vue new file mode 100644 index 00000000..33b45019 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/NavBar.vue @@ -0,0 +1,133 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/components/ui/button/Button.vue b/new-deepnotes/apps/marketing/src/components/ui/button/Button.vue new file mode 100644 index 00000000..1b6a5120 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/ui/button/Button.vue @@ -0,0 +1,31 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/components/ui/button/index.ts b/new-deepnotes/apps/marketing/src/components/ui/button/index.ts new file mode 100644 index 00000000..676a977f --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/ui/button/index.ts @@ -0,0 +1,35 @@ +import type { VariantProps } from 'class-variance-authority' +import { cva } from 'class-variance-authority' + +export { default as Button } from './Button.vue' + +export const buttonVariants = cva( + 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*=size-])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80', + outline: 'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground', + ghost: 'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground', + destructive: 'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + 'default': 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', + 'xs': 'h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*=size-])]:size-3', + 'sm': 'h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*=size-])]:size-3.5', + 'lg': 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', + 'icon': 'size-8', + 'icon-xs': 'size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*=size-])]:size-3', + 'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg', + 'icon-lg': 'size-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +) +export type ButtonVariants = VariantProps diff --git a/new-deepnotes/apps/marketing/src/components/ui/card/Card.vue b/new-deepnotes/apps/marketing/src/components/ui/card/Card.vue new file mode 100644 index 00000000..073e6516 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/ui/card/Card.vue @@ -0,0 +1,21 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/components/ui/card/CardAction.vue b/new-deepnotes/apps/marketing/src/components/ui/card/CardAction.vue new file mode 100644 index 00000000..c2beb206 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/ui/card/CardAction.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/components/ui/card/CardContent.vue b/new-deepnotes/apps/marketing/src/components/ui/card/CardContent.vue new file mode 100644 index 00000000..6270bc46 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/ui/card/CardContent.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/components/ui/card/CardDescription.vue b/new-deepnotes/apps/marketing/src/components/ui/card/CardDescription.vue new file mode 100644 index 00000000..722b2033 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/ui/card/CardDescription.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/components/ui/card/CardFooter.vue b/new-deepnotes/apps/marketing/src/components/ui/card/CardFooter.vue new file mode 100644 index 00000000..ca3936b4 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/ui/card/CardFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/components/ui/card/CardHeader.vue b/new-deepnotes/apps/marketing/src/components/ui/card/CardHeader.vue new file mode 100644 index 00000000..27d56f7e --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/ui/card/CardHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/components/ui/card/CardTitle.vue b/new-deepnotes/apps/marketing/src/components/ui/card/CardTitle.vue new file mode 100644 index 00000000..1f53990e --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/ui/card/CardTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/components/ui/card/index.ts b/new-deepnotes/apps/marketing/src/components/ui/card/index.ts new file mode 100644 index 00000000..73d985f2 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/ui/card/index.ts @@ -0,0 +1,7 @@ +export { default as Card } from './Card.vue' +export { default as CardAction } from './CardAction.vue' +export { default as CardContent } from './CardContent.vue' +export { default as CardDescription } from './CardDescription.vue' +export { default as CardFooter } from './CardFooter.vue' +export { default as CardHeader } from './CardHeader.vue' +export { default as CardTitle } from './CardTitle.vue' diff --git a/new-deepnotes/apps/marketing/src/components/ui/input/Input.vue b/new-deepnotes/apps/marketing/src/components/ui/input/Input.vue new file mode 100644 index 00000000..4ebb6aba --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/ui/input/Input.vue @@ -0,0 +1,31 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/components/ui/input/index.ts b/new-deepnotes/apps/marketing/src/components/ui/input/index.ts new file mode 100644 index 00000000..a691dd6c --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/ui/input/index.ts @@ -0,0 +1 @@ +export { default as Input } from './Input.vue' diff --git a/new-deepnotes/apps/marketing/src/components/ui/switch/Switch.vue b/new-deepnotes/apps/marketing/src/components/ui/switch/Switch.vue new file mode 100644 index 00000000..19e69139 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/ui/switch/Switch.vue @@ -0,0 +1,44 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/components/ui/switch/index.ts b/new-deepnotes/apps/marketing/src/components/ui/switch/index.ts new file mode 100644 index 00000000..87b4b17d --- /dev/null +++ b/new-deepnotes/apps/marketing/src/components/ui/switch/index.ts @@ -0,0 +1 @@ +export { default as Switch } from './Switch.vue' diff --git a/new-deepnotes/apps/marketing/src/composables/useAuthHint.ts b/new-deepnotes/apps/marketing/src/composables/useAuthHint.ts new file mode 100644 index 00000000..93537584 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/composables/useAuthHint.ts @@ -0,0 +1,16 @@ +import { ref, onMounted } from "vue"; + +function readLoggedInCookie(): boolean { + if (typeof document === "undefined") return false; + return document.cookie.split("; ").some((c) => c.startsWith("loggedIn=true")); +} + +export function useAuthHint() { + const isLoggedIn = ref(false); + + onMounted(() => { + isLoggedIn.value = readLoggedInCookie(); + }); + + return { isLoggedIn }; +} diff --git a/new-deepnotes/apps/marketing/src/layouts/DefaultLayout.vue b/new-deepnotes/apps/marketing/src/layouts/DefaultLayout.vue new file mode 100644 index 00000000..a7d703b9 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/layouts/DefaultLayout.vue @@ -0,0 +1,14 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/lib/utils.ts b/new-deepnotes/apps/marketing/src/lib/utils.ts new file mode 100644 index 00000000..88283f01 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/lib/utils.ts @@ -0,0 +1,7 @@ +import type { ClassValue } from "clsx"; +import { clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/new-deepnotes/apps/marketing/src/main.ts b/new-deepnotes/apps/marketing/src/main.ts new file mode 100644 index 00000000..7565b737 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/main.ts @@ -0,0 +1,13 @@ +import "./styles/globals.css"; + +import { ViteSSG } from "vite-ssg"; + +import App from "./App.vue"; +import { routes } from "./router"; + +export const createApp = ViteSSG(App, { + routes, + scrollBehavior(_to, _from, savedPosition) { + return savedPosition ?? { top: 0 }; + }, +}); diff --git a/new-deepnotes/apps/marketing/src/pages/HelpArticlePage.vue b/new-deepnotes/apps/marketing/src/pages/HelpArticlePage.vue new file mode 100644 index 00000000..036dd198 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/pages/HelpArticlePage.vue @@ -0,0 +1,286 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/pages/HelpPage.vue b/new-deepnotes/apps/marketing/src/pages/HelpPage.vue new file mode 100644 index 00000000..cfcf796f --- /dev/null +++ b/new-deepnotes/apps/marketing/src/pages/HelpPage.vue @@ -0,0 +1,166 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/pages/HomePage.vue b/new-deepnotes/apps/marketing/src/pages/HomePage.vue new file mode 100644 index 00000000..18897734 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/pages/HomePage.vue @@ -0,0 +1,190 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/pages/PricingPage.vue b/new-deepnotes/apps/marketing/src/pages/PricingPage.vue new file mode 100644 index 00000000..73bb303a --- /dev/null +++ b/new-deepnotes/apps/marketing/src/pages/PricingPage.vue @@ -0,0 +1,187 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/pages/PrivacyPolicyPage.vue b/new-deepnotes/apps/marketing/src/pages/PrivacyPolicyPage.vue new file mode 100644 index 00000000..3834184c --- /dev/null +++ b/new-deepnotes/apps/marketing/src/pages/PrivacyPolicyPage.vue @@ -0,0 +1,67 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/pages/TermsOfServicePage.vue b/new-deepnotes/apps/marketing/src/pages/TermsOfServicePage.vue new file mode 100644 index 00000000..99277f66 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/pages/TermsOfServicePage.vue @@ -0,0 +1,71 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/pages/WhitepaperPage.vue b/new-deepnotes/apps/marketing/src/pages/WhitepaperPage.vue new file mode 100644 index 00000000..35d20654 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/pages/WhitepaperPage.vue @@ -0,0 +1,186 @@ + + + diff --git a/new-deepnotes/apps/marketing/src/router/index.ts b/new-deepnotes/apps/marketing/src/router/index.ts new file mode 100644 index 00000000..790e02be --- /dev/null +++ b/new-deepnotes/apps/marketing/src/router/index.ts @@ -0,0 +1,39 @@ +import type { RouteRecordRaw } from "vue-router"; + +export const routes: RouteRecordRaw[] = [ + { + path: "/", + name: "home", + component: () => import("@/pages/HomePage.vue"), + }, + { + path: "/pricing", + name: "pricing", + component: () => import("@/pages/PricingPage.vue"), + }, + { + path: "/whitepaper", + name: "whitepaper", + component: () => import("@/pages/WhitepaperPage.vue"), + }, + { + path: "/help", + name: "help", + component: () => import("@/pages/HelpPage.vue"), + }, + { + path: "/help/:slug", + name: "help-article", + component: () => import("@/pages/HelpArticlePage.vue"), + }, + { + path: "/privacy-policy", + name: "privacy-policy", + component: () => import("@/pages/PrivacyPolicyPage.vue"), + }, + { + path: "/terms-of-service", + name: "terms-of-service", + component: () => import("@/pages/TermsOfServicePage.vue"), + }, +]; diff --git a/new-deepnotes/apps/marketing/src/styles/globals.css b/new-deepnotes/apps/marketing/src/styles/globals.css new file mode 100644 index 00000000..b729ba67 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/styles/globals.css @@ -0,0 +1,131 @@ + +@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap'); + +@import "tailwindcss"; + +@import "tw-animate-css"; + +@import "shadcn-vue/tailwind.css"; + +@plugin "@tailwindcss/typography"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --font-sans: 'Geist Variable', sans-serif; + --font-heading: var(--font-sans); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-foreground: var(--foreground); + --color-background: var(--background); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + @apply font-sans; + } +} diff --git a/new-deepnotes/apps/marketing/src/vite-env.d.ts b/new-deepnotes/apps/marketing/src/vite-env.d.ts new file mode 100644 index 00000000..44aa1861 --- /dev/null +++ b/new-deepnotes/apps/marketing/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + /** Absolute or root-relative URL for the signed-in web app (e.g. https://app.example.com or /). */ + readonly VITE_WEB_APP_URL?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/new-deepnotes/apps/marketing/tsconfig.json b/new-deepnotes/apps/marketing/tsconfig.json new file mode 100644 index 00000000..ce0c594d --- /dev/null +++ b/new-deepnotes/apps/marketing/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "verbatimModuleSyntax": true, + "useDefineForClassFields": true, + "types": ["vite/client"], + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"] +} diff --git a/new-deepnotes/apps/marketing/vite.config.ts b/new-deepnotes/apps/marketing/vite.config.ts new file mode 100644 index 00000000..bba02a77 --- /dev/null +++ b/new-deepnotes/apps/marketing/vite.config.ts @@ -0,0 +1,42 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import tailwindcss from "@tailwindcss/vite"; +import vue from "@vitejs/plugin-vue"; +import { defineConfig } from "vite"; + +const appRoot = fileURLToPath(new URL(".", import.meta.url)); + +export default defineConfig({ + envDir: '../..', + plugins: [vue(), tailwindcss()], + resolve: { + alias: { + "@": path.resolve(appRoot, "src"), + }, + }, + ssgOptions: { + script: "async", + includedRoutes(paths) { + const staticPaths = paths.filter((path) => !path.includes(":")); + const helpSlugs = [ + "what-is-deepnotes", + "getting-started", + "creating-notes", + "sharing-pages", + "billing-subscriptions", + "creating-group", + "inviting-users", + "joining-group", + "forgot-password", + "offline-usage", + "multi-page-search", + "roadmap", + "refund-policy", + "subscription-expiration", + ]; + const helpPaths = helpSlugs.map((slug) => `/help/${slug}`); + return [...staticPaths, ...helpPaths]; + }, + }, +}); diff --git a/new-deepnotes/apps/web/README.md b/new-deepnotes/apps/web/README.md new file mode 100644 index 00000000..e10290c9 --- /dev/null +++ b/new-deepnotes/apps/web/README.md @@ -0,0 +1,37 @@ +# @deepnotes/web + +Vue 3 SPA for the greenfield stack. The bundle talks to the API only through [`src/api/`](./src/api/) (OpenAPI-generated `paths` + `openapi-fetch`); it does not import server or Drizzle packages at runtime. + +## Styling + +- **Tailwind CSS v4** with the [Vite plugin](https://tailwindcss.com/docs/installation/framework-guides) (`@tailwindcss/vite` in [`vite.config.ts`](./vite.config.ts)), global entry [`src/styles/globals.css`](./src/styles/globals.css). +- **[shadcn-vue](https://www.shadcn-vue.com/)** (Reka + `components.json`); UI primitives live under [`src/components/ui/`](./src/components/ui). Add more with: + + `pnpm dlx shadcn-vue@latest add --yes` + +- **Imports:** Vite + `tsconfig` path alias `@` โ†’ [`src`](./tsconfig.app.json) (e.g. `@/components/ui/button`). + +## Layout + +- `src/api/` โ€” `createDeepnotesApiClient`, generated types (`pnpm run generate:api-types` when `packages/api` changes). +- `src/features/auth/` โ€” session bootstrap (`/api/sessions/refresh` + `GET /api/users/me` when the `loggedIn` cookie is set), email/password + 2FA step, shared helpers. +- `src/features/home/` โ€” first shell screen after auth. +- `src/features/groups/` โ€” `GET /api/users/me/groups` plus per-group `main-page`, `members`, and `pages` (first window) for a read-only [Groups](src/features/groups/GroupsView.vue) screen (`/groups`, signed-in only). +- `src/features/notifications/` โ€” `GET /api/users/me/notifications` and `POST โ€ฆ/notifications/read` for [Notifications](src/features/notifications/NotificationsView.vue) (`/notifications`, signed-in only; list shows `type` + time, bodies stay encrypted in this MVP). +- `src/router.ts` โ€” `vue-router` history routes. + +## Local dev + +- API base URL defaults to same origin. With `pnpm` dev for this app, Vite proxies `/api` to `http://127.0.0.1:8787` (run `wrangler dev` in `api-worker` there). +- Override with `VITE_API_URL` (no trailing slash) for a different host. + +## Sign-in contract + +- **Password login** sends `loginHash` as standard base64 over the UTF-8 bytes of the password. Any future `POST /api/users` registration UI must use the same preimage so Argon2 verification matches. + +See also [../docs/AUTH_AND_CORS.md](../docs/AUTH_AND_CORS.md). + +## Testing + +- **Vitest** (`pnpm test`): feature unit tests, [client.test.ts](./src/api/client.test.ts) (mocked `fetch`), and [client.contract.test.ts](./src/api/client.contract.test.ts) with **MSW** ([src/test/msw/](./src/test/msw/) handlers for OpenAPI-shaped `GET /api/health` and `GET /api/users/me`). +- **ESLint** (`pnpm lint`): `src/**/*.ts` + `no-restricted-imports` so the app does not import `@deepnotes/api-worker`, `@deepnotes/db`, `@deepnotes/api`, `@deepnotes/session`, or `drizzle-orm` (see [eslint.config.js](./eslint.config.js)). diff --git a/new-deepnotes/apps/web/components.json b/new-deepnotes/apps/web/components.json new file mode 100644 index 00000000..3a2ebf95 --- /dev/null +++ b/new-deepnotes/apps/web/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://shadcn-vue.com/schema.json", + "style": "reka-nova", + "font": "geist-sans", + "typescript": true, + "tailwind": { + "config": "", + "css": "src/styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "composables": "@/composables" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/new-deepnotes/apps/web/e2e/smoke.spec.ts b/new-deepnotes/apps/web/e2e/smoke.spec.ts new file mode 100644 index 00000000..d73be5f8 --- /dev/null +++ b/new-deepnotes/apps/web/e2e/smoke.spec.ts @@ -0,0 +1,38 @@ +import { test, expect } from "@playwright/test"; + +test("home page renders with sign-in link", async ({ page }) => { + await page.goto("/"); + await expect(page.locator("text=Sign in")).toBeVisible(); + await expect(page.locator("text=DeepNotes")).toBeVisible(); +}); + +test("login โ†’ home โ†’ page โ†’ groups โ†’ logout", async ({ page }) => { + // 1. Start at login + await page.goto("/login"); + await expect(page.locator("text=Sign in")).toBeVisible(); + + // 2. Fill in login credentials (requires test user to exist) + await page.fill('input[name="email"]', "test@example.com"); + await page.fill('input[name="password"]', "testpassword123"); + await page.click('button[type="submit"]'); + + // 3. Home should load with user summary + await expect(page.locator("text=Signed in")).toBeVisible(); + + // 4. Navigate to the starting page + const startPageLink = page.locator('a[href^="/pages/"]'); + await expect(startPageLink.first()).toBeVisible(); + await startPageLink.first().click(); + + // 5. Page editor should load + await expect(page.locator("text=Loading pageโ€ฆ")).not.toBeVisible({ timeout: 10000 }); + + // 6. Navigate to Groups + await page.goto("/groups"); + await expect(page.locator("text=Groups")).toBeVisible(); + await expect(page.locator("text=Personal")).toBeVisible(); + + // 7. Logout + await page.click("text=Sign out"); + await expect(page.locator("text=Sign in")).toBeVisible(); +}); diff --git a/new-deepnotes/apps/web/eslint.config.js b/new-deepnotes/apps/web/eslint.config.js new file mode 100644 index 00000000..3bd61000 --- /dev/null +++ b/new-deepnotes/apps/web/eslint.config.js @@ -0,0 +1,48 @@ +import base from "../../eslint.config.js"; + +export default [ + ...base, + { + ignores: [ + "src/api/api-types.generated.ts", + "src/api/openapi.json", + "src/components/ui/**", + ], + }, + { + files: ["src/**/*.ts"], + rules: { + "no-restricted-imports": [ + "error", + { + paths: [ + { + name: "@deepnotes/api-worker", + message: + "Use the OpenAPI client and fetch only; do not import the api-worker app.", + }, + { + name: "@deepnotes/db", + message: "Web must not import the database package; use HTTP only.", + }, + { + name: "@deepnotes/api", + message: + "Use generated src/api types only; do not import @deepnotes/api at runtime (codegen is dev-time).", + }, + { + name: "@deepnotes/session", + message: "Web must not import @deepnotes/session; use @deepnotes/e2ee and HTTP.", + }, + ], + patterns: [ + { + group: ["drizzle-orm", "drizzle-orm/*"], + message: "Web must not import drizzle-orm.", + }, + ], + }, + ], + }, + }, +]; diff --git a/new-deepnotes/apps/web/index.html b/new-deepnotes/apps/web/index.html new file mode 100644 index 00000000..3822097d --- /dev/null +++ b/new-deepnotes/apps/web/index.html @@ -0,0 +1,23 @@ + + + + + + DeepNotes + + + +
+ + + diff --git a/new-deepnotes/apps/web/package.json b/new-deepnotes/apps/web/package.json new file mode 100644 index 00000000..50f4427a --- /dev/null +++ b/new-deepnotes/apps/web/package.json @@ -0,0 +1,84 @@ +{ + "name": "@deepnotes/web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "generate:api-types": "tsx scripts/generate-api-types.mts", + "build": "vite build", + "predev": "node -e \"const fs=require('fs'),s='../../.env',d='.env'; if(fs.existsSync(s)) fs.copyFileSync(s,d);\"", + "dev": "vite", + "lint": "eslint vite.config.ts \"src/**/*.ts\"", + "test": "vitest run", + "typecheck": "vue-tsc --noEmit -p tsconfig.app.json", + "preview": "vite preview" + }, + "dependencies": { + "@deepnotes/collab-wire": "workspace:^", + "@deepnotes/e2ee": "workspace:^", + "@deepnotes/realtime-wire": "workspace:^", + "@lucide/vue": "^1.17.0", + "@tailwindcss/vite": "^4.2.4", + "@tiptap/core": "3.22.4", + "@tiptap/extension-code-block-lowlight": "3.22.4", + "@tiptap/extension-collaboration": "3.22.4", + "@tiptap/extension-collaboration-caret": "3.22.4", + "@tiptap/extension-highlight": "3.22.4", + "@tiptap/extension-horizontal-rule": "3.22.4", + "@tiptap/extension-image": "3.22.4", + "@tiptap/extension-link": "3.22.4", + "@tiptap/extension-placeholder": "3.22.4", + "@tiptap/extension-subscript": "3.22.4", + "@tiptap/extension-superscript": "3.22.4", + "@tiptap/extension-table": "3.22.4", + "@tiptap/extension-table-cell": "3.22.4", + "@tiptap/extension-table-header": "3.22.4", + "@tiptap/extension-table-row": "3.22.4", + "@tiptap/extension-task-item": "3.22.4", + "@tiptap/extension-task-list": "3.22.4", + "@tiptap/extension-text-align": "3.22.4", + "@tiptap/extension-underline": "3.22.4", + "@tiptap/extension-youtube": "3.22.4", + "@tiptap/pm": "3.22.4", + "@tiptap/starter-kit": "3.22.4", + "@tiptap/vue-3": "3.22.4", + "@tiptap/y-tiptap": "3.0.3", + "@vueuse/core": "^14.3.0", + "@vueuse/shared": "14.2.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "highlight.js": "^11.11.1", + "html2canvas": "^1.4.1", + "katex": "0.16.22", + "lowlight": "3.3.0", + "marked": "^18.0.4", + "msgpackr": "^1.11.2", + "nanoid": "^5.1.5", + "openapi-fetch": "^0.17.0", + "reka-ui": "^2.9.8", + "shadcn-vue": "^2.6.2", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.2.4", + "turndown": "^7.2.4", + "tw-animate-css": "^1.4.0", + "vue": "^3.5.13", + "vue-router": "^5.0.6", + "y-protocols": "^1.0.7", + "yjs": "^13.6.30" + }, + "devDependencies": { + "@playwright/test": "^1.52.0", + "@types/node": "^22.14.1", + "@types/turndown": "^5.0.6", + "@vitejs/plugin-vue": "^5.2.3", + "@vue/test-utils": "^2.4.6", + "happy-dom": "^17.4.4", + "msw": "^2.0.0", + "openapi-typescript": "^7.13.0", + "tsx": "^4.21.0", + "typescript": "^5.8.3", + "vite": "^6.3.3", + "vitest": "^3.2.4", + "vue-tsc": "^2.2.10" + } +} diff --git a/new-deepnotes/apps/web/playwright.config.ts b/new-deepnotes/apps/web/playwright.config.ts new file mode 100644 index 00000000..839c7db8 --- /dev/null +++ b/new-deepnotes/apps/web/playwright.config.ts @@ -0,0 +1,33 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./e2e", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: "list", + use: { + baseURL: "http://127.0.0.1:5174", + trace: "on-first-retry", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + webServer: [ + { + command: "pnpm -C ../api-worker dev", + url: "http://127.0.0.1:8787/api/health", + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI, + }, + { + command: "pnpm dev", + url: "http://127.0.0.1:5174", + reuseExistingServer: !process.env.CI, + }, + ], +}); diff --git a/new-deepnotes/apps/web/public/black-logo.png b/new-deepnotes/apps/web/public/black-logo.png new file mode 100644 index 00000000..8166ea5b Binary files /dev/null and b/new-deepnotes/apps/web/public/black-logo.png differ diff --git a/new-deepnotes/apps/web/public/white-logo-outline.webp b/new-deepnotes/apps/web/public/white-logo-outline.webp new file mode 100644 index 00000000..a5cb4ed5 Binary files /dev/null and b/new-deepnotes/apps/web/public/white-logo-outline.webp differ diff --git a/new-deepnotes/apps/web/scripts/generate-api-types.mts b/new-deepnotes/apps/web/scripts/generate-api-types.mts new file mode 100644 index 00000000..2daf4ad8 --- /dev/null +++ b/new-deepnotes/apps/web/scripts/generate-api-types.mts @@ -0,0 +1,20 @@ +import { execSync } from "node:child_process"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { getOpenApiDocument } from "../../../packages/api/src/openapi.ts"; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const webRoot = join(scriptDir, ".."); +const outDir = join(webRoot, "src", "api"); + +mkdirSync(outDir, { recursive: true }); +const jsonPath = join(outDir, "openapi.json"); +const typesPath = join(outDir, "api-types.generated.ts"); + +writeFileSync(jsonPath, `${JSON.stringify(getOpenApiDocument(), null, 2)}\n`); + +execSync( + `pnpm exec openapi-typescript "${jsonPath}" -o "${typesPath}"`, + { cwd: webRoot, stdio: "inherit" }, +); diff --git a/new-deepnotes/apps/web/src/App.vue b/new-deepnotes/apps/web/src/App.vue new file mode 100644 index 00000000..478fc5a8 --- /dev/null +++ b/new-deepnotes/apps/web/src/App.vue @@ -0,0 +1,47 @@ + + + diff --git a/new-deepnotes/apps/web/src/api/api-types.generated.ts b/new-deepnotes/apps/web/src/api/api-types.generated.ts new file mode 100644 index 00000000..2e2ac6ab --- /dev/null +++ b/new-deepnotes/apps/web/src/api/api-types.generated.ts @@ -0,0 +1,6950 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/api/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Health check */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description API is reachable */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HealthResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/sessions/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create session (email + login hash) + * @description Replaces legacy `sessions.login`. Sets httpOnly cookies when implemented. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["SessionLoginRequest"]; + }; + }; + responses: { + /** @description Login succeeded; cookies set. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionLoginSuccess"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Too many failed login attempts (rate limited). */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Register a new account + * @description Replaces legacy `users.account.register`. Creates user, personal group, and first page; sets email verification unless `SEND_EMAILS=false` (then verifies immediately, legacy parity). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserRegisterRequest"]; + }; + }; + responses: { + /** @description User created. `emailVerified` is true when outbound mail is disabled (`SEND_EMAILS=false`). */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserRegisterResponse"]; + }; + }; + /** @description Validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource already exists (e.g. email already registered). */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Email send failed (e.g. Resend API error after user row was created; rare). */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Current user (from access cookie) + * @description Minimal account summary for the authenticated user (`accessToken` cookie). + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Authenticated user. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserMeResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + /** + * Delete current account (password confirmation) + * @description Replaces legacy `users.account.delete`. Requires `accessToken` cookie and correct `loginHash` in the JSON body. Clears session cookies on success. Optional Stripe customer deletion is handled by the deployment (not part of OpenAPI). + */ + delete: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserAccountDeleteRequest"]; + }; + }; + responses: { + /** @description Account removed; session cookies cleared (same names as login). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wrong password, ownership constraint, or validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/{userId}/public-keyring": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * User public keyring (E2EE box key) + * @description Returns `users.public_keyring` for wrapping group secrets when sending invitations. Any authenticated user may read (same visibility as legacy KeyDB `user:*:public-keyring`). + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Base64 libsodium public keyring bytes. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserPublicKeyringResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/groups": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List group IDs for the current user + * @description Replaces legacy `users.pages.getGroupIds`. Returns `group_id` values from `group_members` ordered by recent activity (desc). + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ordered group ids. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserGroupIdsResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/pages/starting": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Starting page id for the current user + * @description Replaces legacy `users.pages.getStartingPageId` (reads `users.starting_page_id`). + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Nanoid of the userโ€™s starting page. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserStartingPageResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/pages/path": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Breadcrumb path from a page to the personal main page + * @description Replaces legacy `users.pages.getCurrentPath`. Uses `users_pages.last_parent_id` and may repair a missing parent link once (legacy KeyDB behavior). + */ + get: { + parameters: { + query: { + initialPageId: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ordered page ids from root (personal main) to `initialPageId`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserCurrentPathResponse"]; + }; + }; + /** @description Missing or invalid `initialPageId` query parameter. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/pages/recent": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Recent page ids for the current user + * @description Returns `users.recent_page_ids` (most recently bumped first). Complements POST mutations that do not echo the list. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ordered recent page nanoids. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserPageIdListResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/pages/favorites": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Favorite page ids for the current user + * @description Returns `users.favorite_page_ids` (newest additions first per add order). Complements POST mutations. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ordered favorite page nanoids. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserPageIdListResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + /** + * Add favorite pages + * @description Replaces legacy `users.pages.addFavoritePages`. Favorites are stored in Postgres (`users.favorite_page_ids`); legacy used KeyDB only. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserPageIdsBody"]; + }; + }; + responses: { + /** @description Favorites merged (order: new ids first, then existing). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/pages/recent/remove": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Remove page ids from recent list + * @description Replaces legacy `users.pages.removeRecentPages`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserPageIdsBody"]; + }; + }; + responses: { + /** @description Updated `users.recent_page_ids`. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/pages/recent/clear": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Clear recent pages + * @description Replaces legacy `users.pages.clearRecentPages`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Recent list emptied. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/pages/favorites/remove": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Remove favorite pages + * @description Replaces legacy `users.pages.removeFavoritePages`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserPageIdsBody"]; + }; + }; + responses: { + /** @description Favorites updated. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/pages/favorites/clear": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Clear favorite pages + * @description Replaces legacy `users.pages.clearFavoritePages`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Favorites emptied. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/defaults/note": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Update encrypted default note template + * @description Replaces legacy `users.pages.setEncryptedDefaultNote`. + */ + patch: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserDefaultNotePatch"]; + }; + }; + responses: { + /** @description `users.encrypted_default_note` updated. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + trace?: never; + }; + "/api/users/me/defaults/arrow": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Update encrypted default arrow template + * @description Replaces legacy `users.pages.setEncryptedDefaultArrow`. + */ + patch: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserDefaultArrowPatch"]; + }; + }; + responses: { + /** @description `users.encrypted_default_arrow` updated. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + trace?: never; + }; + "/api/users/me/notifications": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Load notifications for the current user + * @description Replaces legacy `users.pages.notifications.load`. Ciphertext fields are base64 in JSON. + */ + get: { + parameters: { + query?: { + lastNotificationId?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Window of notifications and optional `lastNotificationRead`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserNotificationsLoadResponse"]; + }; + }; + /** @description Invalid query parameters. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/notifications/read": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Mark all notifications as read + * @description Replaces legacy `users.pages.notifications.markAsRead`. Sets `users.last_notification_read` to the latest linked notification id. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Read cursor updated (no-op if user has no notifications). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/main-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get the group main page id + * @description Replaces legacy `groups.getMainPageId` (KeyDB `main-page-id`). Source: `groups.main_page_id`. Requires `viewGroupPages` (same as listing pages). + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Main page id for the group. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GroupMainPageResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/members": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List user ids (members, requests, invitations) + * @description Replaces legacy `groups.getUserIds`: union of `group_members`, `group_join_requests`, and `group_join_invitations` for the group. Requires `viewGroupMembers` (not granted for public read without membership). + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Distinct user ids (unordered). */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GroupMemberUserIdsResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/members/detail": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Group membership detail (roles + pending invites/requests) + * @description Returns the callerโ€™s role, full member list with roles, pending invitations and non-rejected join requests, and group flags (`groupIsPublic`, `joinRequestsAllowed`). Requires `viewGroupMembers` and an active membership row. + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Structured membership for admin UIs. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GroupMembersDetailResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/invite-crypto-bootstrap": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Encrypted group key material for invitation flows (managers) + * @description Returns ciphertext the callerโ€™s browser unwraps to build `POST โ€ฆ/join-invitations` and `POST โ€ฆ/join-requests/{userId}/accept` bodies. Requires membership with manager role (owner/admin/moderator). With optional `inviteeUserId` query, also returns public keyrings for E2EE group notifications (legacy WS step 2). + */ + get: { + parameters: { + query?: { + inviteeUserId?: string; + }; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Encrypted keyrings + group public key. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GroupInviteCryptoBootstrapResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/collab-crypto-context": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Group ciphertext for page move / re-key (edit pages) + * @description Returns `groups.encrypted_content_keyring`, optional `access_keyring`, and the callerโ€™s `group_members.encrypted_access_keyring` so the SPA can unwrap `GroupContentKeyring` when moving a page into this group. Requires `editGroupPages` and membership. + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Encrypted blobs for destination-side symmetric wrap. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GroupCollabCryptoContextResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/privacy/make-private-bootstrap": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Read model for making a public group private (re-key) + * @description Returns member/invite/request/page ciphertext ids and user public keys matching legacy WS `groups.privacy.makePrivate` step 1, so the browser can build `POST โ€ฆ/privacy/private`. Requires Pro, `editGroupSettings`, and a public group (`access_keyring` set). + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Key rotation bootstrap (base64 fields). */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GroupPrivacyMakePrivateBootstrapResponse"]; + }; + }; + /** @description Group is already private. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/public-keyring": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Group public keyring for name encryption + * @description Returns `groups.public_keyring` when the caller may encrypt a display name for invite accept or join-request flows: active member, pending invitation, or join requests allowed and not yet a member. + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Base64 group box public keyring. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GroupPublicKeyringResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/pages": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List page IDs in a group + * @description Replaces legacy `groups.getPages` (`optionalAuthProcedure`). Cookie optional: **public** groups (`access_keyring` set) may be listed without a session (same as legacy anonymous `viewGroupPages`). Private groups require `viewGroupPages`. Optional `lastPageId` cursor for pagination (newest `last_activity_date` first). Omits soft-deleted pages (`permanent_deletion_date` set). + */ + get: { + parameters: { + query?: { + lastPageId?: string; + }; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Page id window (max 20) and `hasMore`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GroupPagesListResponse"]; + }; + }; + /** @description Invalid `lastPageId` (not in group). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + /** + * Create a page in a group + * @description Replaces legacy `pages.create`. For an **existing** group, `parentPageId` must be a page in that group and the caller needs `editGroupPages`. With optional `groupCreation`, path `groupId` is a **new** nanoid (no row yet), `parentPageId` is a page in the userโ€™s **personal** group, and the body includes the same ciphertext as `PageMoveGroupCreationRequest` โ€” Pro only; creates the `groups` + owner `group_members` rows then the first page (legacy parity). The 50 free-page cap applies to nonโ€‘Pro users for normal creates. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupPageCreateRequest"]; + }; + }; + responses: { + /** @description Page and `users_pages` row created. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GroupPageCreateResponse"]; + }; + }; + /** @description Invalid parent page or body. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Enable group password (Pro) + * @description Replaces `groups.password.enable`. Argon2id is applied on the server to the provided `groupPasswordHash` material (base64) and stored encrypted. Requires `editGroupSettings` and a Pro plan. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupPasswordEnableRequest"]; + }; + }; + responses: { + /** @description Password protection enabled. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already protected or bad password material. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + /** + * Disable group password (not Pro check in legacy for disable-only) + * @description Replaces `groups.password.disable`. Verifies the current group password, removes server-side group password, updates `groupEncryptedContentKeyring`. + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupPasswordDisableRequest"]; + }; + }; + responses: { + /** @description Password protection disabled. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wrong password, or not protected. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + /** + * Change group password (Pro) + * @description Replaces `groups.password.change`. Verifies the current group password, then re-wraps the content keyring. + */ + patch: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupPasswordChangeRequest"]; + }; + }; + responses: { + /** @description Password updated. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wrong password, or group not protected. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + trace?: never; + }; + "/api/groups/{groupId}/privacy/public": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Make group public (Pro) + * @description Replaces `groups.privacy.makePublic`. Sets `access_keyring` and clears member/invite `encrypted_access_keyring`. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupPrivacyPublicRequest"]; + }; + }; + responses: { + /** @description Group is public. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already public. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/privacy/join-requests": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Allow or reject join requests (Pro) + * @description Replaces `groups.privacy.setJoinRequestsAllowed`. + */ + patch: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupPrivacyJoinRequestsPatch"]; + }; + }; + responses: { + /** @description Setting updated. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + trace?: never; + }; + "/api/groups/{groupId}/privacy/private": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Make group private (Pro) โ€” full re-key payload + * @description Replaces legacy WS `groups.privacy.makePrivate` (step 2 `rotateGroupKeys`) in one request. Clears `access_keyring` when `groupAccessKeyring` is omitted. Member / invitation / request / page record keys must match the DB exactly. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupPrivacyPrivateRequest"]; + }; + }; + responses: { + /** @description Group is private; ciphertext updated. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already private or payload key sets do not match group. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Soft-delete group (grace period) + * @description Replaces `groups.deletion.delete`. Sets `permanent_deletion_date` ~1 month ahead. + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deletion scheduled. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already soft-deleted. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Restore a soft-deleted group + * @description Replaces `groups.deletion.restore` during the grace period (`permanent_deletion_date` in the future). + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Group removed from scheduled deletion. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Not soft-deleted, or no longer in grace period. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/purge": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Permanently mark group deleted (purge active or grace state) + * @description Replaces `groups.deletion.deletePermanently` โ€” `permanent_deletion_date` set in the past (legacy). + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Purge recorded. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already purged. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/join-invitations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Send a group join invitation (Pro) + * @description Replaces legacy WS `groups.joinInvitations.send` step 1. Deletes a conflicting join request for the invitee. For private groups, `encryptedAccessKeyring` is required; for public groups it is stored as null. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupJoinInvitationSendRequest"]; + }; + }; + responses: { + /** @description Invitation created. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already invited, already a member, or missing keyring for private group. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/join-invitations/me/accept": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Accept a pending join invitation (Pro) + * @description Replaces legacy WS `groups.joinInvitations.accept` step 1. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupJoinInvitationAcceptRequest"]; + }; + }; + responses: { + /** @description Invitation consumed; user added to `group_members`. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/join-invitations/me/reject": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Reject a pending join invitation + * @description Replaces legacy WS `groups.joinInvitations.reject` step 1 (no Pro check in legacy). + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Invitation removed. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No pending invitation. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/join-invitations/{userId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Cancel a join invitation (Pro) + * @description Replaces legacy WS `groups.joinInvitations.cancel` step 1. Path `userId` is the invitee. Requires permission to manage the invited role. + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Invitation removed. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/join-requests": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Send a join request (Pro) + * @description Replaces legacy WS `groups.joinRequests.send` step 1. Requires `are_join_requests_allowed` on the group. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupJoinRequestSendRequest"]; + }; + }; + responses: { + /** @description Join request created. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already pending. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/join-requests/{userId}/accept": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Accept a join request (Pro) + * @description Replaces legacy WS `groups.joinRequests.accept` step 1. Path `userId` is the requester. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + userId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupJoinRequestAcceptRequest"]; + }; + }; + responses: { + /** @description Requester added to `group_members`. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No pending request or missing access keyring for private group. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/join-requests/{userId}/reject": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Reject a join request (Pro) + * @description Replaces legacy WS `groups.joinRequests.reject` step 1. Sets `rejected` on the request (legacy does not delete the row). + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Request marked rejected. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No pending request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/join-requests/me/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Cancel own join request (Pro) + * @description Replaces legacy WS `groups.joinRequests.cancel` step 1. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Join request row deleted. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No pending request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/members/{userId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Remove a member (or leave) + * @description Replaces legacy WS `groups.removeUser` step 1. Callers may remove themselves without `canManageRole` on others. + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Membership removed. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Cannot remove the last owner. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + /** + * Change a member's role (Pro) + * @description Replaces legacy WS `groups.changeUserRole` step 1. + */ + patch: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + userId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupMemberRolePatchRequest"]; + }; + }; + responses: { + /** @description `group_members.role` updated. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + trace?: never; + }; + "/api/pages/{pageId}/move": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Move page (optionally create group, re-key, set main) + * @description Replaces `websocket/pages/move` โ€” Pro-only; `editGroupSettings` on the page's current group, `editGroupPages` on destination unless `groupCreation` creates it. `reencrypt` is required when the page changes group (Yjs `page_updates` replaced with a single index-0 row; snapshots updated by id). + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PageMoveRequest"]; + }; + }; + responses: { + /** @description Move completed. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No-op move, or invalid payload. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}/bump": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Bump page (recents, activity, optional breadcrumb parent) + * @description Replaces `pages.bump` โ€” `users` starting + recents, optional `users_pages.last_parent_id` when the parent chain ends at the personal main page (`lastParentId` walk). + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PageBumpRequest"]; + }; + }; + responses: { + /** @description Bumped (best-effort; loop in chain exits without updating parent). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid parent (chain does not resolve to main page). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}/collab-updates": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List encrypted Yjs page updates (Postgres) + * @description Bootstrap for the editor: returns `page_updates` rows for the page, ordered by `index`. Use `sinceIndex` to paginate incrementally (default limit 100, max 500). Does not use legacy Redis collab cache โ€” Postgres only. Full duplex collab remains a separate WebSocket track (Phase 3). + */ + get: { + parameters: { + query?: { + sinceIndex?: string; + limit?: string; + }; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current ciphertext chain. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PageCollabUpdatesGetResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + /** + * Append page updates (optimistic concurrency) + * @description Appends ciphertext rows to `page_updates`. `expectedLastIndex` must match the current max index (or null when empty). **409** when another writer advanced the chain โ€” client should re-GET and retry. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PageCollabUpdatesAppendRequest"]; + }; + }; + responses: { + /** @description Updates persisted. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad index sequence or wrong `expectedLastIndex` for an empty page. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Stale `expectedLastIndex` (concurrent append). */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}/backlinks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List backlinks pointing to this page + * @description Returns source page IDs that link to this page, ordered by most recent activity. + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Backlink source page IDs. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PageBacklinkListResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + /** + * Create page backlink (source โ†’ this page as target) + * @description Replaces `pages.backlinks.create`. Path `pageId` is the **target**; body has `sourcePageId`. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PageBacklinkCreateRequest"]; + }; + }; + responses: { + /** @description Backlink created or activity updated (upsert). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Source and target identical. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}/backlinks/{targetPageId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Delete backlink from source page to target page + * @description Replaces `pages.backlinks.delete`. Path `pageId` is **source**; `targetPageId` is the link target (legacy input names). + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + targetPageId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Backlink removed. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}/snapshots": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List page snapshot metadata (Pro) + * @description Returns snapshot ids and timestamps for manual and pre-restore snapshots (no ciphertext). + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Newest first. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PageSnapshotListResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + /** + * Save encrypted page snapshot (Pro) + * @description Replaces `pages.snapshots.save` โ€” asserts Pro plan (legacy `assertUserSubscribed`). + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PageSnapshotSaveRequest"]; + }; + }; + responses: { + /** @description Snapshot id */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PageSnapshotCreateResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}/snapshots/{snapshotId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Load page snapshot ciphertext (Pro) */ + get: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + snapshotId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ciphertext (base64 fields). */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PageSnapshotLoadResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + /** Delete a page snapshot */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + snapshotId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Snapshot removed. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Soft-delete page (grace period) */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deletion scheduled (not main page). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already deleted, or is group main page. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}/restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Restore a soft-deleted page */ + post: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Page restored in grace. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Not deleted, or free page past purge date. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}/purge": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Permanently mark page deleted; refunds free page when applicable */ + post: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Purge recorded. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Is main page, or already purged. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Change password (re-wrap keyrings) + * @description Replaces legacy WebSocket `users.account.changePassword`. Requires `accessToken`; verifies `oldLoginHash`; stores keyrings encrypted with the new password (`userEncrypted*` are plaintext keyrings from the client, same as registration). Invalidates all sessions and clears cookies โ€” client must log in again. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserPasswordChangeRequest"]; + }; + }; + responses: { + /** @description Password updated; all sessions invalidated; session cookies cleared. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wrong current password or invalid key material. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/email-change": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Request account email change (6-digit code email) + * @description Replaces legacy `users.account.emailChange.request`. Verifies `oldLoginHash` and that the new address is not already registered. When outbound email is enabled, sends a 6-digit code. When `SEND_EMAILS=false` (e.g. local), returns 200 with `emailVerificationCode` instead of emailing. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserEmailChangeRequest"]; + }; + }; + responses: { + /** @description Out-of-band dev response when `SEND_EMAILS=false` (verification code not emailed). */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserEmailChangeRequestResponse"]; + }; + }; + /** @description Code emailed to the new address; pending change stored on the user row. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wrong password, address in use, or validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Email send failed (e.g. Resend) after the pending state was written. */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/email-change/confirm": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Confirm email change (re-wrap keyrings, new password) + * @description Replaces legacy WebSocket `users.account.emailChange.finish` (step 1 + 2 in one). Verifies 6-digit code and `oldLoginHash`, then applies new email + new password-encrypted keyrings, invalidates sessions, clears cookies; optional Stripe customer email update in the deployment (not in OpenAPI). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserEmailChangeConfirmRequest"]; + }; + }; + responses: { + /** @description Email updated; sessions cleared; re-login required. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wrong code, wrong password, no pending change, or invalid keyrings. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/2fa/enable/request": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Start 2FA setup (TOTP secret + otpauth URI) + * @description Replaces `users.account.twoFactorAuth.enable.request`. Stores a pending encrypted authenticator secret; client shows QR from `keyUri` or `secret`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["User2faPasswordBody"]; + }; + }; + responses: { + /** @description Secret generated; not yet enabled until `โ€ฆ/enable/finish`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["User2faEnableRequestResponse"]; + }; + }; + /** @description Validation error, or 2FA already fully enabled. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/2fa/enable/finish": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Complete 2FA setup (TOTP + recovery codes) + * @description Replaces `users.account.twoFactorAuth.enable.finish`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["User2faEnableFinishRequest"]; + }; + }; + responses: { + /** @description 2FA enabled; one-time recovery codes returned. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["User2faRecoveryCodesResponse"]; + }; + }; + /** @description Wrong password, wrong TOTP, or already enabled. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/2fa/load": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Reveal TOTP secret and otpauth URI (after password check) + * @description Replaces `users.account.twoFactorAuth.load` (legacy tRPC had `loginHash` in the query; this API uses a JSON body on POST to avoid putting secrets in query strings or logs). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["User2faPasswordBody"]; + }; + }; + responses: { + /** @description Secret and `keyUri` for re-provisioning an authenticator. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["User2faEnableRequestResponse"]; + }; + }; + /** @description Wrong password or 2FA not enabled. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/2fa/recovery-codes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Regenerate recovery codes + * @description Replaces `users.account.twoFactorAuth.generateRecoveryCodes`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["User2faPasswordBody"]; + }; + }; + responses: { + /** @description New recovery codes (previous codes invalidated). */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["User2faRecoveryCodesResponse"]; + }; + }; + /** @description Wrong password or 2FA not enabled. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/2fa/devices/forget": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Mark all user devices as not trusted + * @description Replaces `users.account.twoFactorAuth.forgetTrustedDevices`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["User2faPasswordBody"]; + }; + }; + responses: { + /** @description `devices.trusted` cleared for this user. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wrong password or 2FA not enabled. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/2fa/disable": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Disable 2FA + * @description Replaces `users.account.twoFactorAuth.disable`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["User2faPasswordBody"]; + }; + }; + responses: { + /** @description 2FA disabled; authenticator and recovery material cleared. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wrong password or 2FA not enabled. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/email-verification/resend": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Resend email verification (public, by email) + * @description Replaces legacy `users.account.resendVerificationEmail`. Uses Resend when `SEND_EMAILS` is not `false`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["EmailVerificationResendRequest"]; + }; + }; + responses: { + /** @description Email sent (or accepted by provider). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation error or outbound email disabled for this environment. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource already exists (e.g. email already registered). */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Email provider (Resend) request failed. */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/email-verification/confirm": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Confirm email with nanoid code + * @description Replaces legacy `users.account.verifyEmail` (public). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["EmailVerificationConfirmRequest"]; + }; + }; + responses: { + /** @description Email verified; account updated. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid or expired code. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/sessions/refresh": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Rotate access token using refresh cookie + * @description Replaces legacy `sessions.refresh`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description New session key and cookies. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionRefreshSuccess"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/sessions/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Invalidate session and clear cookies + * @description Replaces legacy `sessions.logout`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Logged out (cookies cleared). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/sessions/demo": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create demo user and session + * @description Replaces legacy `sessions.startDemo`. Request body will match registration key material once defined. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["SessionDemoRequest"]; + }; + }; + responses: { + /** @description Demo user created; same response shape as login. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionLoginSuccess"]; + }; + }; + /** @description Validation error (e.g. unsupported group password on demo). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/billing/stripe/checkout-session": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create Stripe Checkout (subscription) session + * @description Replaces legacy `users.account.stripe.createCheckoutSession`. Resolves or creates a Stripe customer from `users.customer_id` and decrypted account email, then returns a hosted Checkout URL. Requires verified email. Demo accounts receive **403**. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["StripeCheckoutSessionRequest"]; + }; + }; + responses: { + /** @description Checkout session URL (hosted Stripe page). */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StripeCheckoutSessionResponse"]; + }; + }; + /** @description Already subscribed, or validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/billing/stripe/portal-session": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create Stripe Customer Portal session + * @description Replaces legacy `users.account.stripe.createPortalSession`. Requires a `users.customer_id` (create checkout first or migrate from legacy). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Portal session URL. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StripePortalSessionResponse"]; + }; + }; + /** @description No Stripe customer on file. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/webhooks/stripe": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Stripe webhooks (signed raw body) + * @description Replaces the legacy Fastify `POST /stripe/webhook` handler. Send the **raw** request body; verification uses the `Stripe-Signature` header and `STRIPE_WEBHOOK_SECRET`. Handles `customer.subscription.updated` and `customer.subscription.deleted` by updating `users.plan` and `users.subscription_id` via `users.customer_id`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Event acknowledged. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Missing or invalid signature. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + HealthResponse: { + /** @enum {string} */ + status: "ok"; + service: string; + }; + SessionLoginSuccess: { + userId: string; + sessionId: string; + /** + * Format: byte + * @description Base64-encoded session symmetric key. + */ + sessionKey: string; + personalGroupId: string; + /** Format: byte */ + publicKeyring: string; + /** Format: byte */ + encryptedPrivateKeyring: string; + /** Format: byte */ + encryptedSymmetricKeyring: string; + /** + * Format: byte + * @description Argon2 salt (base64). + */ + passwordSalt?: string; + }; + SessionErrorResponse: { + code: string; + message: string; + }; + ServiceUnavailableResponse: { + /** @enum {string} */ + code: "SERVICE_UNAVAILABLE"; + message: string; + }; + SessionLoginRequest: { + email: string | "demo"; + /** + * Format: byte + * @description Base64-encoded login hash (legacy wire used binary; prefer standard base64 in JSON). + */ + loginHash: string; + rememberSession: boolean; + authenticatorToken?: string; + rememberDevice?: boolean; + recoveryCode?: string; + }; + UserRegisterResponse: { + userId: string; + emailVerified: boolean; + }; + SessionDemoGroupCreation: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedName: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupPasswordHash?: string; + groupIsPublic: boolean; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupAccessKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedInternalKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedContentKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupPublicKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedPrivateKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupOwnerEncryptedName: string; + }; + SessionDemoPageCreation: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedSymmetricKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedRelativeTitle: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedAbsoluteTitle: string; + }; + SessionDemoRequest: { + userId: string; + groupId: string; + pageId: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userPublicKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedPrivateKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedSymmetricKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedName: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedDefaultNote: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedDefaultArrow: string; + groupCreation: components["schemas"]["SessionDemoGroupCreation"]; + pageCreation: components["schemas"]["SessionDemoPageCreation"]; + }; + UserRegisterRequest: components["schemas"]["SessionDemoRequest"] & { + /** Format: email */ + email: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + loginHash: string; + }; + UserMeResponse: { + userId: string; + emailVerified: boolean; + demo: boolean; + personalGroupId: string; + /** + * Format: byte + * @description Base64 ciphertext of the user's default note template (msgpack). + */ + encryptedDefaultNote: string; + /** + * Format: byte + * @description Base64 ciphertext of the user's default arrow template (msgpack). + */ + encryptedDefaultArrow: string; + }; + UserPublicKeyringResponse: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + publicKeyring: string; + }; + UserGroupIdsResponse: { + groupIds: string[]; + }; + UserStartingPageResponse: { + startingPageId: string; + }; + UserCurrentPathResponse: { + pathPageIds: string[]; + }; + UserPageIdListResponse: { + pageIds: string[]; + }; + UserPageIdsBody: { + pageIds: string[]; + }; + UserDefaultNotePatch: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedDefaultNote: string; + }; + UserDefaultArrowPatch: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedDefaultArrow: string; + }; + UserNotificationItem: { + id: number; + type: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedSymmetricKey: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedContent: string; + dateTime: string; + }; + UserNotificationsLoadResponse: { + items: components["schemas"]["UserNotificationItem"][]; + hasMore: boolean; + lastNotificationRead?: number | null; + }; + GroupMainPageResponse: { + mainPageId: string; + }; + GroupMemberUserIdsResponse: { + userIds: string[]; + }; + /** @enum {string} */ + GroupMemberRole: "owner" | "admin" | "moderator" | "member" | "viewer"; + GroupMemberRow: { + userId: string; + role: components["schemas"]["GroupMemberRole"]; + }; + GroupPendingInvitationRow: { + userId: string; + role: components["schemas"]["GroupMemberRole"]; + }; + GroupPendingJoinRequestRow: { + userId: string; + }; + GroupMembersDetailResponse: { + viewerUserId: string; + viewerRole: components["schemas"]["GroupMemberRole"]; + groupIsPublic: boolean; + joinRequestsAllowed: boolean; + members: components["schemas"]["GroupMemberRow"][]; + pendingInvitations: components["schemas"]["GroupPendingInvitationRow"][]; + pendingJoinRequests: components["schemas"]["GroupPendingJoinRequestRow"][]; + }; + NotificationRecipientPublicKeyringRow: { + /** + * @description 21-character nanoid (URL-safe alphabet). + * @example V1StGXR8_Z5jdHi6B-myT + */ + userId: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + publicKeyring: string; + }; + GroupInviteCryptoBootstrapResponse: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupPublicKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupAccessKeyring: string | null; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + memberEncryptedAccessKeyring: string | null; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + memberEncryptedInternalKeyring: string; + notificationRecipientPublicKeyrings?: components["schemas"]["NotificationRecipientPublicKeyringRow"][]; + }; + GroupCollabCryptoContextResponse: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedContentKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupAccessKeyring: string | null; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + memberEncryptedAccessKeyring: string | null; + }; + GroupPrivacyMakePrivateMemberBootstrap: { + /** + * Format: byte + * @description Invitee/member `users.public_keyring` (base64). + */ + publicKeyring: string; + /** + * Format: byte + * @description `group_members.encrypted_name` (base64) or null. + */ + encryptedName: string | null; + }; + GroupPrivacyMakePrivateInvitationBootstrap: { + /** Format: byte */ + publicKeyring: string; + /** Format: byte */ + encryptedName: string; + }; + GroupPrivacyMakePrivateJoinRequestBootstrap: { + /** Format: byte */ + encryptedName: string; + }; + GroupPrivacyMakePrivatePageBootstrap: { + /** Format: byte */ + encryptedSymmetricKeyring: string; + }; + GroupPrivacyMakePrivateBootstrapResponse: { + /** Format: byte */ + groupAccessKeyring: string | null; + /** Format: byte */ + groupEncryptedName: string; + /** Format: byte */ + groupEncryptedContentKeyring: string; + /** Format: byte */ + groupPublicKeyring: string; + /** Format: byte */ + groupEncryptedPrivateKeyring: string; + /** + * Format: byte + * @description Viewerโ€™s `group_members.encrypted_access_keyring` (often null when public). + */ + groupEncryptedAccessKeyring: string | null; + /** Format: byte */ + groupEncryptedInternalKeyring: string; + groupMembers: { + [key: string]: components["schemas"]["GroupPrivacyMakePrivateMemberBootstrap"]; + }; + groupJoinInvitations: { + [key: string]: components["schemas"]["GroupPrivacyMakePrivateInvitationBootstrap"]; + }; + groupJoinRequests: { + [key: string]: components["schemas"]["GroupPrivacyMakePrivateJoinRequestBootstrap"]; + }; + groupPages: { + [key: string]: components["schemas"]["GroupPrivacyMakePrivatePageBootstrap"]; + }; + }; + GroupPublicKeyringResponse: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupPublicKeyring: string; + }; + GroupPagesListResponse: { + pageIds: string[]; + hasMore: boolean; + }; + GroupPageCreateResponse: { + pageId: string; + numFreePages?: number; + }; + PageMoveGroupCreationRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedName: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupPasswordHash?: string; + groupIsPublic: boolean; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupAccessKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedInternalKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedContentKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupPublicKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedPrivateKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupOwnerEncryptedName: string; + }; + GroupPageCreateRequest: { + /** + * @description 21-character nanoid (URL-safe alphabet). + * @example V1StGXR8_Z5jdHi6B-myT + */ + parentPageId: string; + /** + * @description 21-character nanoid (URL-safe alphabet). + * @example V1StGXR8_Z5jdHi6B-myT + */ + pageId: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedSymmetricKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedRelativeTitle: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedAbsoluteTitle: string; + groupCreation?: components["schemas"]["PageMoveGroupCreationRequest"]; + }; + GroupPasswordEnableRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupPasswordHash: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedContentKeyring: string; + }; + GroupPasswordChangeRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupCurrentPasswordHash: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupNewPasswordHash: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedContentKeyring: string; + }; + GroupPasswordDisableRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupPasswordHash: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedContentKeyring: string; + }; + GroupPrivacyPublicRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + accessKeyring: string; + }; + GroupPrivacyJoinRequestsPatch: { + areJoinRequestsAllowed: boolean; + }; + GroupPrivacyPrivateMember: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedAccessKeyring?: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedInternalKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedName: string | null; + }; + GroupPrivacyPrivateInvitation: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedAccessKeyring?: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedInternalKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedName: string; + }; + GroupPrivacyPrivateJoinRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedName: string; + }; + GroupPrivacyPrivatePage: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedSymmetricKeyring: string; + }; + GroupPrivacyPrivateRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupAccessKeyring?: string; + /** + * Format: byte + * @description Standard base64; empty string means zero-length binary. + */ + groupEncryptedName: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedContentKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupPublicKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedPrivateKeyring: string; + groupMembers: { + [key: string]: components["schemas"]["GroupPrivacyPrivateMember"]; + }; + groupJoinInvitations: { + [key: string]: components["schemas"]["GroupPrivacyPrivateInvitation"]; + }; + groupJoinRequests: { + [key: string]: components["schemas"]["GroupPrivacyPrivateJoinRequest"]; + }; + groupPages: { + [key: string]: components["schemas"]["GroupPrivacyPrivatePage"]; + }; + }; + /** @enum {string} */ + DeepnotesNotificationType: "group-request-sent" | "group-request-canceled" | "group-request-accepted" | "group-request-rejected" | "group-invitation-sent" | "group-invitation-canceled" | "group-invitation-accepted" | "group-invitation-rejected" | "group-member-role-changed" | "group-member-removed"; + GroupInviteNotificationPayload: { + type: components["schemas"]["DeepnotesNotificationType"]; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedContent: string; + recipients: { + [key: string]: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedSymmetricKey: string; + }; + }; + }; + GroupJoinInvitationSendRequest: { + /** + * @description 21-character nanoid (URL-safe alphabet). + * @example V1StGXR8_Z5jdHi6B-myT + */ + inviteeUserId: string; + invitationRole: components["schemas"]["GroupMemberRole"]; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedAccessKeyring?: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedInternalKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedName: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedNameForUser: string; + notifications?: components["schemas"]["GroupInviteNotificationPayload"][]; + }; + GroupJoinInvitationAcceptRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedName: string; + }; + GroupJoinRequestSendRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedUserName: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedUserNameForUser: string; + }; + GroupJoinRequestAcceptRequest: { + targetRole: components["schemas"]["GroupMemberRole"]; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedAccessKeyring?: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedInternalKeyring: string; + }; + GroupMemberRolePatchRequest: { + role: components["schemas"]["GroupMemberRole"]; + }; + PageMoveReencryptRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedSymmetricKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedRelativeTitle: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedAbsoluteTitle: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedUpdate: string; + /** @default {} */ + pageEncryptedSnapshots: { + [key: string]: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedSymmetricKey: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedData: string; + }; + }; + }; + PageMoveRequest: { + /** + * @description 21-character nanoid (URL-safe alphabet). + * @example V1StGXR8_Z5jdHi6B-myT + */ + destGroupId: string; + setAsMainPage: boolean; + groupCreation?: components["schemas"]["PageMoveGroupCreationRequest"]; + reencrypt?: components["schemas"]["PageMoveReencryptRequest"]; + }; + PageBumpRequest: { + parentPageId?: string; + }; + PageCollabUpdatesGetResponse: { + /** @description Max `index` in the database, or null if there are no rows. */ + lastIndex: number | null; + updates: { + index: number; + /** + * Format: byte + * @description Base64 ciphertext (`page_updates.encrypted_data`). + */ + encryptedData: string; + }[]; + /** @description Owning group (`pages.group_id`) for access-key + content-key unwrap. */ + groupId: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedSymmetricKeyring: string; + /** + * Format: byte + * @description `pages.encrypted_relative_title` (re-key on cross-group move). + */ + pageEncryptedRelativeTitle: string; + /** + * Format: byte + * @description `pages.encrypted_absolute_title` (re-key on cross-group move). + */ + pageEncryptedAbsoluteTitle: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedContentKeyring: string; + /** + * Format: byte + * @description Public group `access_keyring` bytes when set; otherwise null (use member blob). + */ + groupAccessKeyring: string | null; + /** + * Format: byte + * @description `group_members.encrypted_access_keyring` for this user when present. + */ + memberEncryptedAccessKeyring: string | null; + }; + PageCollabUpdateItemInput: { + /** @description Monotonic index (legacy collab / `page_updates.index`, often Yjs clock). */ + index: number; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedData: string; + }; + PageCollabUpdatesAppendRequest: { + /** @description Must match `lastIndex` from GET (`null` when the page has no updates yet). */ + expectedLastIndex: number | null; + updates: components["schemas"]["PageCollabUpdateItemInput"][]; + }; + PageBacklinkCreateRequest: { + /** + * @description 21-character nanoid (URL-safe alphabet). + * @example V1StGXR8_Z5jdHi6B-myT + */ + sourcePageId: string; + }; + PageBacklinkListResponse: { + /** @description Page IDs that link to this page, ordered by most recent activity. */ + sourcePageIds: string[]; + }; + PageSnapshotListItem: { + snapshotId: string; + /** @description ISO-8601 timestamp from `page_snapshots.creation_date`. */ + creationDate: string; + /** @enum {string} */ + type: "manual" | "pre-restore"; + }; + PageSnapshotListResponse: { + snapshots: components["schemas"]["PageSnapshotListItem"][]; + }; + PageSnapshotCreateResponse: { + snapshotId: string; + }; + PageSnapshotSaveRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedSymmetricKey: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedData: string; + preRestore?: boolean; + }; + PageSnapshotLoadResponse: { + encryptedSymmetricKey: string | null; + /** + * Format: byte + * @description Base64 ciphertext. + */ + encryptedData: string; + }; + UserPasswordChangeRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + oldLoginHash: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + newLoginHash: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedPrivateKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedSymmetricKeyring: string; + }; + UserEmailChangeRequestResponse: { + emailVerificationCode: string; + }; + UserEmailChangeRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + oldLoginHash: string; + /** Format: email */ + newEmail: string; + }; + UserEmailChangeConfirmRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + oldLoginHash: string; + emailVerificationCode: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + newLoginHash: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedPrivateKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedSymmetricKeyring: string; + }; + UserAccountDeleteRequest: { + /** + * Format: byte + * @description Base64-encoded login hash (same semantics as `POST /api/sessions/login`). + */ + loginHash: string; + }; + User2faEnableRequestResponse: { + secret: string; + /** @description otpauth:// URI for authenticator apps. */ + keyUri: string; + }; + User2faPasswordBody: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + loginHash: string; + }; + User2faRecoveryCodesResponse: { + recoveryCodes: string[]; + }; + User2faEnableFinishRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + loginHash: string; + authenticatorToken: string; + }; + EmailVerificationResendRequest: { + /** Format: email */ + email: string; + }; + EmailVerificationConfirmRequest: { + emailVerificationCode: string; + }; + SessionRefreshSuccess: { + /** Format: byte */ + oldSessionKey: string; + /** Format: byte */ + newSessionKey: string; + }; + StripeCheckoutSessionResponse: { + /** + * Format: uri + * @example https://checkout.stripe.com/c/pay/... + */ + checkoutSessionUrl: string; + }; + StripeCheckoutSessionRequest: { + /** + * @description Defaults to `monthly` when omitted (legacy tRPC). + * @enum {string} + */ + billingFrequency?: "monthly" | "yearly"; + }; + StripePortalSessionResponse: { + /** + * Format: uri + * @example https://billing.stripe.com/p/session/... + */ + portalSessionUrl: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export type operations = Record; diff --git a/new-deepnotes/apps/web/src/api/client.contract.test.ts b/new-deepnotes/apps/web/src/api/client.contract.test.ts new file mode 100644 index 00000000..a01f2ed4 --- /dev/null +++ b/new-deepnotes/apps/web/src/api/client.contract.test.ts @@ -0,0 +1,155 @@ +import { http, HttpResponse } from "msw"; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "vitest"; + +import { + contractApiBaseUrl, + deepnotesSessionContractHandlers, + mswSessionLoginSuccess, +} from "../test/msw/deepnotes-handlers"; +import { mswServer } from "../test/msw/node"; +import { createDeepnotesApiClient } from "./client"; + +describe("createDeepnotesApiClient (MSW contract)", () => { + beforeAll(() => { + mswServer.listen({ onUnhandledRequest: "error" }); + }); + + afterEach(() => { + mswServer.resetHandlers(); + }); + + afterAll(() => { + mswServer.close(); + }); + + it("GET /api/health returns 200 and HealthResponse shape", async () => { + const client = createDeepnotesApiClient(contractApiBaseUrl); + const { data, error, response } = await client.GET("/api/health"); + + expect(response.status).toBe(200); + expect(error).toBeUndefined(); + expect(data).toEqual({ status: "ok", service: "msw" }); + }); + + it("GET /api/users/me returns 200 and UserMeResponse; fetch uses credentials", async () => { + const client = createDeepnotesApiClient(contractApiBaseUrl); + const { data, error, response } = await client.GET("/api/users/me"); + + expect(response.status).toBe(200); + expect(error).toBeUndefined(); + expect(data).toMatchObject({ + userId: "u_msw", + emailVerified: true, + personalGroupId: "g_msw", + }); + }); + + it("maps 401 error body for /api/users/me", async () => { + mswServer.use( + http.get(`${contractApiBaseUrl}/api/users/me`, () => + HttpResponse.json( + { code: "UNAUTHORIZED", message: "Not logged in." }, + { status: 401 }, + ), + ), + ); + + const client = createDeepnotesApiClient(contractApiBaseUrl); + const { data, error, response } = await client.GET("/api/users/me"); + + expect(response.status).toBe(401); + expect(data).toBeUndefined(); + expect(error).toMatchObject({ + code: "UNAUTHORIZED", + message: "Not logged in.", + }); + }); + + describe("session POST routes", () => { + beforeEach(() => { + mswServer.use(...deepnotesSessionContractHandlers()); + }); + + it("POST /api/sessions/logout returns 204 with empty body", async () => { + const client = createDeepnotesApiClient(contractApiBaseUrl); + const { data, error, response } = await client.POST( + "/api/sessions/logout", + {}, + ); + expect(response.status).toBe(204); + expect(error).toBeUndefined(); + expect(data).toBeUndefined(); + }); + + it("POST /api/sessions/refresh returns 200 and SessionRefreshSuccess", async () => { + const client = createDeepnotesApiClient(contractApiBaseUrl); + const { data, error, response } = await client.POST( + "/api/sessions/refresh", + {}, + ); + expect(response.status).toBe(200); + expect(error).toBeUndefined(); + expect(data).toMatchObject({ + oldSessionKey: "dGVzdA==", + newSessionKey: "dGVzdGI=", + }); + }); + + it("POST /api/sessions/login returns 200 and SessionLoginSuccess", async () => { + const client = createDeepnotesApiClient(contractApiBaseUrl); + const { loginPreimageFromPassword, uint8ToBase64 } = await import( + "../features/auth/bytes" + ); + const body = { + email: "a@example.com", + loginHash: uint8ToBase64(loginPreimageFromPassword("pw")), + rememberSession: false, + }; + const { data, error, response } = await client.POST("/api/sessions/login", { + body, + }); + expect(response.status).toBe(200); + expect(error).toBeUndefined(); + expect(data).toMatchObject({ ...mswSessionLoginSuccess }); + }); + + it("POST /api/sessions/login maps 401 + SessionErrorResponse (2FA)", async () => { + mswServer.use( + http.post(`${contractApiBaseUrl}/api/sessions/login`, () => + HttpResponse.json( + { + code: "UNAUTHORIZED", + message: "Requires two-factor authentication.", + }, + { status: 401 }, + ), + ), + ); + const client = createDeepnotesApiClient(contractApiBaseUrl); + const { loginPreimageFromPassword, uint8ToBase64 } = await import( + "../features/auth/bytes" + ); + const { data, error, response } = await client.POST("/api/sessions/login", { + body: { + email: "a@example.com", + loginHash: uint8ToBase64(loginPreimageFromPassword("pw")), + rememberSession: true, + }, + }); + expect(response.status).toBe(401); + expect(data).toBeUndefined(); + expect(error).toMatchObject({ + code: "UNAUTHORIZED", + message: "Requires two-factor authentication.", + }); + }); + }); +}); diff --git a/new-deepnotes/apps/web/src/api/client.test.ts b/new-deepnotes/apps/web/src/api/client.test.ts new file mode 100644 index 00000000..16d43ed9 --- /dev/null +++ b/new-deepnotes/apps/web/src/api/client.test.ts @@ -0,0 +1,28 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { createDeepnotesApiClient } from "./client"; + +describe("createDeepnotesApiClient", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("sends credentials: include for cookie session auth", async () => { + const fetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ status: "ok" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + vi.stubGlobal("fetch", fetchMock); + + const client = createDeepnotesApiClient("https://api.example"); + const res = await client.GET("/api/health"); + + expect(res.response.status).toBe(200); + expect(fetchMock).toHaveBeenCalledTimes(1); + const [input] = fetchMock.mock.calls[0] as [Request]; + expect(input.credentials).toBe("include"); + expect(input.url).toContain("/api/health"); + }); +}); diff --git a/new-deepnotes/apps/web/src/api/client.ts b/new-deepnotes/apps/web/src/api/client.ts new file mode 100644 index 00000000..e8d0781b --- /dev/null +++ b/new-deepnotes/apps/web/src/api/client.ts @@ -0,0 +1,23 @@ +import createClient from "openapi-fetch"; + +import type { paths } from "./api-types.generated"; + +/** + * Base URL for the DeepNotes HTTP API (no trailing slash). Empty string uses the + * current origin (Vite dev server proxy or Pages same-origin API). + */ +export function resolveApiBaseUrl(): string { + const raw = import.meta.env.VITE_API_URL ?? ""; + return raw.replace(/\/$/, ""); +} + +/** Typed OpenAPI client with `credentials: "include"` for httpOnly session cookies. */ +export function createDeepnotesApiClient(baseUrl?: string) { + const root = baseUrl ?? resolveApiBaseUrl(); + return createClient({ + baseUrl: root, + credentials: "include", + }); +} + +export type DeepnotesApiClient = ReturnType; diff --git a/new-deepnotes/apps/web/src/api/index.ts b/new-deepnotes/apps/web/src/api/index.ts new file mode 100644 index 00000000..87a461dc --- /dev/null +++ b/new-deepnotes/apps/web/src/api/index.ts @@ -0,0 +1,6 @@ +export { + createDeepnotesApiClient, + resolveApiBaseUrl, + type DeepnotesApiClient, +} from "./client"; +export type { components, paths } from "./api-types.generated"; diff --git a/new-deepnotes/apps/web/src/api/openapi.json b/new-deepnotes/apps/web/src/api/openapi.json new file mode 100644 index 00000000..7ec6c556 --- /dev/null +++ b/new-deepnotes/apps/web/src/api/openapi.json @@ -0,0 +1,7469 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "DeepNotes API", + "version": "0.0.0", + "description": "Greenfield HTTP API (REST + OpenAPI). Legacy /trpc is not a compatibility target. Billing in this product is Stripe on the web only; native IAP/RevenueCat surfaces are out of scope (see RESTART_PLAN)." + }, + "servers": [ + { + "url": "/" + } + ], + "components": { + "schemas": { + "HealthResponse": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "ok" + ] + }, + "service": { + "type": "string" + } + }, + "required": [ + "status", + "service" + ] + }, + "SessionLoginSuccess": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "sessionId": { + "type": "string" + }, + "sessionKey": { + "type": "string", + "format": "byte", + "description": "Base64-encoded session symmetric key." + }, + "personalGroupId": { + "type": "string" + }, + "publicKeyring": { + "type": "string", + "format": "byte" + }, + "encryptedPrivateKeyring": { + "type": "string", + "format": "byte" + }, + "encryptedSymmetricKeyring": { + "type": "string", + "format": "byte" + }, + "passwordSalt": { + "type": "string", + "format": "byte", + "description": "Argon2 salt (base64)." + } + }, + "required": [ + "userId", + "sessionId", + "sessionKey", + "personalGroupId", + "publicKeyring", + "encryptedPrivateKeyring", + "encryptedSymmetricKeyring" + ] + }, + "SessionErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ] + }, + "ServiceUnavailableResponse": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "SERVICE_UNAVAILABLE" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ] + }, + "SessionLoginRequest": { + "type": "object", + "properties": { + "email": { + "anyOf": [ + { + "type": "string", + "format": "email" + }, + { + "type": "string", + "enum": [ + "demo" + ] + } + ] + }, + "loginHash": { + "type": "string", + "format": "byte", + "description": "Base64-encoded login hash (legacy wire used binary; prefer standard base64 in JSON)." + }, + "rememberSession": { + "type": "boolean" + }, + "authenticatorToken": { + "type": "string" + }, + "rememberDevice": { + "type": "boolean" + }, + "recoveryCode": { + "type": "string", + "pattern": "^[a-f0-9]{32}$" + } + }, + "required": [ + "email", + "loginHash", + "rememberSession" + ] + }, + "UserRegisterResponse": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "emailVerified": { + "type": "boolean" + } + }, + "required": [ + "userId", + "emailVerified" + ] + }, + "SessionDemoGroupCreation": { + "type": "object", + "properties": { + "groupEncryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupPasswordHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupIsPublic": { + "type": "boolean" + }, + "groupAccessKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedInternalKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedContentKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupPublicKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedPrivateKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupOwnerEncryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "groupEncryptedName", + "groupIsPublic", + "groupAccessKeyring", + "groupEncryptedInternalKeyring", + "groupEncryptedContentKeyring", + "groupPublicKeyring", + "groupEncryptedPrivateKeyring", + "groupOwnerEncryptedName" + ] + }, + "SessionDemoPageCreation": { + "type": "object", + "properties": { + "pageEncryptedSymmetricKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedRelativeTitle": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedAbsoluteTitle": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "pageEncryptedSymmetricKeyring", + "pageEncryptedRelativeTitle", + "pageEncryptedAbsoluteTitle" + ] + }, + "SessionDemoRequest": { + "type": "object", + "properties": { + "userId": { + "type": "string", + "minLength": 21, + "maxLength": 21, + "pattern": "^[A-Za-z0-9_-]{21}$" + }, + "groupId": { + "type": "string", + "minLength": 21, + "maxLength": 21, + "pattern": "^[A-Za-z0-9_-]{21}$" + }, + "pageId": { + "type": "string", + "minLength": 21, + "maxLength": 21, + "pattern": "^[A-Za-z0-9_-]{21}$" + }, + "userPublicKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedPrivateKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedSymmetricKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedDefaultNote": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedDefaultArrow": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupCreation": { + "$ref": "#/components/schemas/SessionDemoGroupCreation" + }, + "pageCreation": { + "$ref": "#/components/schemas/SessionDemoPageCreation" + } + }, + "required": [ + "userId", + "groupId", + "pageId", + "userPublicKeyring", + "userEncryptedPrivateKeyring", + "userEncryptedSymmetricKeyring", + "userEncryptedName", + "userEncryptedDefaultNote", + "userEncryptedDefaultArrow", + "groupCreation", + "pageCreation" + ] + }, + "UserRegisterRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/SessionDemoRequest" + }, + { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "loginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "email", + "loginHash" + ] + } + ] + }, + "UserMeResponse": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "emailVerified": { + "type": "boolean" + }, + "demo": { + "type": "boolean" + }, + "personalGroupId": { + "type": "string" + }, + "encryptedDefaultNote": { + "type": "string", + "format": "byte", + "description": "Base64 ciphertext of the user's default note template (msgpack)." + }, + "encryptedDefaultArrow": { + "type": "string", + "format": "byte", + "description": "Base64 ciphertext of the user's default arrow template (msgpack)." + } + }, + "required": [ + "userId", + "emailVerified", + "demo", + "personalGroupId", + "encryptedDefaultNote", + "encryptedDefaultArrow" + ] + }, + "UserPublicKeyringResponse": { + "type": "object", + "properties": { + "publicKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "publicKeyring" + ] + }, + "UserGroupIdsResponse": { + "type": "object", + "properties": { + "groupIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "groupIds" + ] + }, + "UserStartingPageResponse": { + "type": "object", + "properties": { + "startingPageId": { + "type": "string" + } + }, + "required": [ + "startingPageId" + ] + }, + "UserCurrentPathResponse": { + "type": "object", + "properties": { + "pathPageIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "pathPageIds" + ] + }, + "UserPageIdListResponse": { + "type": "object", + "properties": { + "pageIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "pageIds" + ] + }, + "UserPageIdsBody": { + "type": "object", + "properties": { + "pageIds": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "minItems": 1 + } + }, + "required": [ + "pageIds" + ] + }, + "UserDefaultNotePatch": { + "type": "object", + "properties": { + "userEncryptedDefaultNote": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "userEncryptedDefaultNote" + ] + }, + "UserDefaultArrowPatch": { + "type": "object", + "properties": { + "userEncryptedDefaultArrow": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "userEncryptedDefaultArrow" + ] + }, + "UserNotificationItem": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "encryptedSymmetricKey": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedContent": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "dateTime": { + "type": "string" + } + }, + "required": [ + "id", + "type", + "encryptedSymmetricKey", + "encryptedContent", + "dateTime" + ] + }, + "UserNotificationsLoadResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserNotificationItem" + } + }, + "hasMore": { + "type": "boolean" + }, + "lastNotificationRead": { + "type": "integer", + "nullable": true + } + }, + "required": [ + "items", + "hasMore" + ] + }, + "GroupMainPageResponse": { + "type": "object", + "properties": { + "mainPageId": { + "type": "string" + } + }, + "required": [ + "mainPageId" + ] + }, + "GroupMemberUserIdsResponse": { + "type": "object", + "properties": { + "userIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "userIds" + ] + }, + "GroupMemberRole": { + "type": "string", + "enum": [ + "owner", + "admin", + "moderator", + "member", + "viewer" + ] + }, + "GroupMemberRow": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/GroupMemberRole" + } + }, + "required": [ + "userId", + "role" + ] + }, + "GroupPendingInvitationRow": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/GroupMemberRole" + } + }, + "required": [ + "userId", + "role" + ] + }, + "GroupPendingJoinRequestRow": { + "type": "object", + "properties": { + "userId": { + "type": "string" + } + }, + "required": [ + "userId" + ] + }, + "GroupMembersDetailResponse": { + "type": "object", + "properties": { + "viewerUserId": { + "type": "string" + }, + "viewerRole": { + "$ref": "#/components/schemas/GroupMemberRole" + }, + "groupIsPublic": { + "type": "boolean" + }, + "joinRequestsAllowed": { + "type": "boolean" + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupMemberRow" + } + }, + "pendingInvitations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupPendingInvitationRow" + } + }, + "pendingJoinRequests": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupPendingJoinRequestRow" + } + } + }, + "required": [ + "viewerUserId", + "viewerRole", + "groupIsPublic", + "joinRequestsAllowed", + "members", + "pendingInvitations", + "pendingJoinRequests" + ] + }, + "NotificationRecipientPublicKeyringRow": { + "type": "object", + "properties": { + "userId": { + "type": "string", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "publicKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "userId", + "publicKeyring" + ] + }, + "GroupInviteCryptoBootstrapResponse": { + "type": "object", + "properties": { + "groupPublicKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupAccessKeyring": { + "type": "string", + "nullable": true, + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "memberEncryptedAccessKeyring": { + "type": "string", + "nullable": true, + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "memberEncryptedInternalKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "notificationRecipientPublicKeyrings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationRecipientPublicKeyringRow" + } + } + }, + "required": [ + "groupPublicKeyring", + "groupAccessKeyring", + "memberEncryptedAccessKeyring", + "memberEncryptedInternalKeyring" + ] + }, + "GroupCollabCryptoContextResponse": { + "type": "object", + "properties": { + "groupEncryptedContentKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupAccessKeyring": { + "type": "string", + "nullable": true, + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "memberEncryptedAccessKeyring": { + "type": "string", + "nullable": true, + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "groupEncryptedContentKeyring", + "groupAccessKeyring", + "memberEncryptedAccessKeyring" + ] + }, + "GroupPrivacyMakePrivateMemberBootstrap": { + "type": "object", + "properties": { + "publicKeyring": { + "type": "string", + "format": "byte", + "description": "Invitee/member `users.public_keyring` (base64)." + }, + "encryptedName": { + "type": "string", + "nullable": true, + "format": "byte", + "description": "`group_members.encrypted_name` (base64) or null." + } + }, + "required": [ + "publicKeyring", + "encryptedName" + ] + }, + "GroupPrivacyMakePrivateInvitationBootstrap": { + "type": "object", + "properties": { + "publicKeyring": { + "type": "string", + "format": "byte" + }, + "encryptedName": { + "type": "string", + "format": "byte" + } + }, + "required": [ + "publicKeyring", + "encryptedName" + ] + }, + "GroupPrivacyMakePrivateJoinRequestBootstrap": { + "type": "object", + "properties": { + "encryptedName": { + "type": "string", + "format": "byte" + } + }, + "required": [ + "encryptedName" + ] + }, + "GroupPrivacyMakePrivatePageBootstrap": { + "type": "object", + "properties": { + "encryptedSymmetricKeyring": { + "type": "string", + "format": "byte" + } + }, + "required": [ + "encryptedSymmetricKeyring" + ] + }, + "GroupPrivacyMakePrivateBootstrapResponse": { + "type": "object", + "properties": { + "groupAccessKeyring": { + "type": "string", + "nullable": true, + "format": "byte" + }, + "groupEncryptedName": { + "type": "string", + "format": "byte" + }, + "groupEncryptedContentKeyring": { + "type": "string", + "format": "byte" + }, + "groupPublicKeyring": { + "type": "string", + "format": "byte" + }, + "groupEncryptedPrivateKeyring": { + "type": "string", + "format": "byte" + }, + "groupEncryptedAccessKeyring": { + "type": "string", + "nullable": true, + "format": "byte", + "description": "Viewerโ€™s `group_members.encrypted_access_keyring` (often null when public)." + }, + "groupEncryptedInternalKeyring": { + "type": "string", + "format": "byte" + }, + "groupMembers": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/GroupPrivacyMakePrivateMemberBootstrap" + } + }, + "groupJoinInvitations": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/GroupPrivacyMakePrivateInvitationBootstrap" + } + }, + "groupJoinRequests": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/GroupPrivacyMakePrivateJoinRequestBootstrap" + } + }, + "groupPages": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/GroupPrivacyMakePrivatePageBootstrap" + } + } + }, + "required": [ + "groupAccessKeyring", + "groupEncryptedName", + "groupEncryptedContentKeyring", + "groupPublicKeyring", + "groupEncryptedPrivateKeyring", + "groupEncryptedAccessKeyring", + "groupEncryptedInternalKeyring", + "groupMembers", + "groupJoinInvitations", + "groupJoinRequests", + "groupPages" + ] + }, + "GroupPublicKeyringResponse": { + "type": "object", + "properties": { + "groupPublicKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "groupPublicKeyring" + ] + }, + "GroupPagesListResponse": { + "type": "object", + "properties": { + "pageIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "hasMore": { + "type": "boolean" + } + }, + "required": [ + "pageIds", + "hasMore" + ] + }, + "GroupPageCreateResponse": { + "type": "object", + "properties": { + "pageId": { + "type": "string" + }, + "numFreePages": { + "type": "integer" + } + }, + "required": [ + "pageId" + ] + }, + "PageMoveGroupCreationRequest": { + "type": "object", + "properties": { + "groupEncryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupPasswordHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupIsPublic": { + "type": "boolean" + }, + "groupAccessKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedInternalKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedContentKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupPublicKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedPrivateKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupOwnerEncryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "groupEncryptedName", + "groupIsPublic", + "groupAccessKeyring", + "groupEncryptedInternalKeyring", + "groupEncryptedContentKeyring", + "groupPublicKeyring", + "groupEncryptedPrivateKeyring", + "groupOwnerEncryptedName" + ] + }, + "GroupPageCreateRequest": { + "type": "object", + "properties": { + "parentPageId": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "pageId": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "pageEncryptedSymmetricKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedRelativeTitle": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedAbsoluteTitle": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupCreation": { + "$ref": "#/components/schemas/PageMoveGroupCreationRequest" + } + }, + "required": [ + "parentPageId", + "pageId", + "pageEncryptedSymmetricKeyring", + "pageEncryptedRelativeTitle", + "pageEncryptedAbsoluteTitle" + ] + }, + "GroupPasswordEnableRequest": { + "type": "object", + "properties": { + "groupPasswordHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedContentKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "groupPasswordHash", + "groupEncryptedContentKeyring" + ] + }, + "GroupPasswordChangeRequest": { + "type": "object", + "properties": { + "groupCurrentPasswordHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupNewPasswordHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedContentKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "groupCurrentPasswordHash", + "groupNewPasswordHash", + "groupEncryptedContentKeyring" + ] + }, + "GroupPasswordDisableRequest": { + "type": "object", + "properties": { + "groupPasswordHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedContentKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "groupPasswordHash", + "groupEncryptedContentKeyring" + ] + }, + "GroupPrivacyPublicRequest": { + "type": "object", + "properties": { + "accessKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "accessKeyring" + ] + }, + "GroupPrivacyJoinRequestsPatch": { + "type": "object", + "properties": { + "areJoinRequestsAllowed": { + "type": "boolean" + } + }, + "required": [ + "areJoinRequestsAllowed" + ] + }, + "GroupPrivacyPrivateMember": { + "type": "object", + "properties": { + "encryptedAccessKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedInternalKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedName": { + "type": "string", + "nullable": true, + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "encryptedInternalKeyring", + "encryptedName" + ] + }, + "GroupPrivacyPrivateInvitation": { + "type": "object", + "properties": { + "encryptedAccessKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedInternalKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "encryptedInternalKeyring", + "encryptedName" + ] + }, + "GroupPrivacyPrivateJoinRequest": { + "type": "object", + "properties": { + "encryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "encryptedName" + ] + }, + "GroupPrivacyPrivatePage": { + "type": "object", + "properties": { + "encryptedSymmetricKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "encryptedSymmetricKeyring" + ] + }, + "GroupPrivacyPrivateRequest": { + "type": "object", + "properties": { + "groupAccessKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedName": { + "type": "string", + "format": "byte", + "description": "Standard base64; empty string means zero-length binary." + }, + "groupEncryptedContentKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupPublicKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedPrivateKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupMembers": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/GroupPrivacyPrivateMember" + } + }, + "groupJoinInvitations": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/GroupPrivacyPrivateInvitation" + } + }, + "groupJoinRequests": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/GroupPrivacyPrivateJoinRequest" + } + }, + "groupPages": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/GroupPrivacyPrivatePage" + } + } + }, + "required": [ + "groupEncryptedName", + "groupEncryptedContentKeyring", + "groupPublicKeyring", + "groupEncryptedPrivateKeyring", + "groupMembers", + "groupJoinInvitations", + "groupJoinRequests", + "groupPages" + ] + }, + "DeepnotesNotificationType": { + "type": "string", + "enum": [ + "group-request-sent", + "group-request-canceled", + "group-request-accepted", + "group-request-rejected", + "group-invitation-sent", + "group-invitation-canceled", + "group-invitation-accepted", + "group-invitation-rejected", + "group-member-role-changed", + "group-member-removed" + ] + }, + "GroupInviteNotificationPayload": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/DeepnotesNotificationType" + }, + "encryptedContent": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "recipients": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "encryptedSymmetricKey": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "encryptedSymmetricKey" + ] + } + } + }, + "required": [ + "type", + "encryptedContent", + "recipients" + ] + }, + "GroupJoinInvitationSendRequest": { + "type": "object", + "properties": { + "inviteeUserId": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "invitationRole": { + "$ref": "#/components/schemas/GroupMemberRole" + }, + "encryptedAccessKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedInternalKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedNameForUser": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "notifications": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupInviteNotificationPayload" + } + } + }, + "required": [ + "inviteeUserId", + "invitationRole", + "encryptedInternalKeyring", + "userEncryptedName", + "userEncryptedNameForUser" + ] + }, + "GroupJoinInvitationAcceptRequest": { + "type": "object", + "properties": { + "userEncryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "userEncryptedName" + ] + }, + "GroupJoinRequestSendRequest": { + "type": "object", + "properties": { + "encryptedUserName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedUserNameForUser": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "encryptedUserName", + "encryptedUserNameForUser" + ] + }, + "GroupJoinRequestAcceptRequest": { + "type": "object", + "properties": { + "targetRole": { + "$ref": "#/components/schemas/GroupMemberRole" + }, + "encryptedAccessKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedInternalKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "targetRole", + "encryptedInternalKeyring" + ] + }, + "GroupMemberRolePatchRequest": { + "type": "object", + "properties": { + "role": { + "$ref": "#/components/schemas/GroupMemberRole" + } + }, + "required": [ + "role" + ] + }, + "PageMoveReencryptRequest": { + "type": "object", + "properties": { + "pageEncryptedSymmetricKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedRelativeTitle": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedAbsoluteTitle": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedUpdate": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedSnapshots": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "encryptedSymmetricKey": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedData": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "encryptedSymmetricKey", + "encryptedData" + ] + }, + "default": {} + } + }, + "required": [ + "pageEncryptedSymmetricKeyring", + "pageEncryptedRelativeTitle", + "pageEncryptedAbsoluteTitle", + "pageEncryptedUpdate" + ] + }, + "PageMoveRequest": { + "type": "object", + "properties": { + "destGroupId": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "setAsMainPage": { + "type": "boolean" + }, + "groupCreation": { + "$ref": "#/components/schemas/PageMoveGroupCreationRequest" + }, + "reencrypt": { + "$ref": "#/components/schemas/PageMoveReencryptRequest" + } + }, + "required": [ + "destGroupId", + "setAsMainPage" + ] + }, + "PageBumpRequest": { + "type": "object", + "properties": { + "parentPageId": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$" + } + } + }, + "PageCollabUpdatesGetResponse": { + "type": "object", + "properties": { + "lastIndex": { + "type": "integer", + "nullable": true, + "minimum": 0, + "description": "Max `index` in the database, or null if there are no rows." + }, + "updates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "index": { + "type": "integer", + "minimum": 0 + }, + "encryptedData": { + "type": "string", + "format": "byte", + "description": "Base64 ciphertext (`page_updates.encrypted_data`)." + } + }, + "required": [ + "index", + "encryptedData" + ] + } + }, + "groupId": { + "type": "string", + "description": "Owning group (`pages.group_id`) for access-key + content-key unwrap." + }, + "pageEncryptedSymmetricKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedRelativeTitle": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "`pages.encrypted_relative_title` (re-key on cross-group move)." + }, + "pageEncryptedAbsoluteTitle": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "`pages.encrypted_absolute_title` (re-key on cross-group move)." + }, + "groupEncryptedContentKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupAccessKeyring": { + "type": "string", + "nullable": true, + "minLength": 1, + "format": "byte", + "description": "Public group `access_keyring` bytes when set; otherwise null (use member blob)." + }, + "memberEncryptedAccessKeyring": { + "type": "string", + "nullable": true, + "minLength": 1, + "format": "byte", + "description": "`group_members.encrypted_access_keyring` for this user when present." + } + }, + "required": [ + "lastIndex", + "updates", + "groupId", + "pageEncryptedSymmetricKeyring", + "pageEncryptedRelativeTitle", + "pageEncryptedAbsoluteTitle", + "groupEncryptedContentKeyring", + "groupAccessKeyring", + "memberEncryptedAccessKeyring" + ] + }, + "PageCollabUpdateItemInput": { + "type": "object", + "properties": { + "index": { + "type": "integer", + "minimum": 0, + "description": "Monotonic index (legacy collab / `page_updates.index`, often Yjs clock)." + }, + "encryptedData": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "index", + "encryptedData" + ] + }, + "PageCollabUpdatesAppendRequest": { + "type": "object", + "properties": { + "expectedLastIndex": { + "type": "integer", + "nullable": true, + "minimum": 0, + "description": "Must match `lastIndex` from GET (`null` when the page has no updates yet)." + }, + "updates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PageCollabUpdateItemInput" + }, + "minItems": 1 + } + }, + "required": [ + "expectedLastIndex", + "updates" + ] + }, + "PageBacklinkCreateRequest": { + "type": "object", + "properties": { + "sourcePageId": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + } + }, + "required": [ + "sourcePageId" + ] + }, + "PageBacklinkListResponse": { + "type": "object", + "properties": { + "sourcePageIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Page IDs that link to this page, ordered by most recent activity." + } + }, + "required": [ + "sourcePageIds" + ] + }, + "PageSnapshotListItem": { + "type": "object", + "properties": { + "snapshotId": { + "type": "string" + }, + "creationDate": { + "type": "string", + "description": "ISO-8601 timestamp from `page_snapshots.creation_date`." + }, + "type": { + "type": "string", + "enum": [ + "manual", + "pre-restore" + ] + } + }, + "required": [ + "snapshotId", + "creationDate", + "type" + ] + }, + "PageSnapshotListResponse": { + "type": "object", + "properties": { + "snapshots": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PageSnapshotListItem" + } + } + }, + "required": [ + "snapshots" + ] + }, + "PageSnapshotCreateResponse": { + "type": "object", + "properties": { + "snapshotId": { + "type": "string" + } + }, + "required": [ + "snapshotId" + ] + }, + "PageSnapshotSaveRequest": { + "type": "object", + "properties": { + "encryptedSymmetricKey": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedData": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "preRestore": { + "type": "boolean" + } + }, + "required": [ + "encryptedSymmetricKey", + "encryptedData" + ] + }, + "PageSnapshotLoadResponse": { + "type": "object", + "properties": { + "encryptedSymmetricKey": { + "type": "string", + "nullable": true + }, + "encryptedData": { + "type": "string", + "format": "byte", + "description": "Base64 ciphertext." + } + }, + "required": [ + "encryptedSymmetricKey", + "encryptedData" + ] + }, + "UserPasswordChangeRequest": { + "type": "object", + "properties": { + "oldLoginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "newLoginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedPrivateKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedSymmetricKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "oldLoginHash", + "newLoginHash", + "userEncryptedPrivateKeyring", + "userEncryptedSymmetricKeyring" + ] + }, + "UserEmailChangeRequestResponse": { + "type": "object", + "properties": { + "emailVerificationCode": { + "type": "string", + "pattern": "^\\d{6}$" + } + }, + "required": [ + "emailVerificationCode" + ] + }, + "UserEmailChangeRequest": { + "type": "object", + "properties": { + "oldLoginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "newEmail": { + "type": "string", + "format": "email" + } + }, + "required": [ + "oldLoginHash", + "newEmail" + ] + }, + "UserEmailChangeConfirmRequest": { + "type": "object", + "properties": { + "oldLoginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "emailVerificationCode": { + "type": "string", + "pattern": "^\\d{6}$" + }, + "newLoginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedPrivateKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedSymmetricKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "oldLoginHash", + "emailVerificationCode", + "newLoginHash", + "userEncryptedPrivateKeyring", + "userEncryptedSymmetricKeyring" + ] + }, + "UserAccountDeleteRequest": { + "type": "object", + "properties": { + "loginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Base64-encoded login hash (same semantics as `POST /api/sessions/login`)." + } + }, + "required": [ + "loginHash" + ] + }, + "User2faEnableRequestResponse": { + "type": "object", + "properties": { + "secret": { + "type": "string" + }, + "keyUri": { + "type": "string", + "description": "otpauth:// URI for authenticator apps." + } + }, + "required": [ + "secret", + "keyUri" + ] + }, + "User2faPasswordBody": { + "type": "object", + "properties": { + "loginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "loginHash" + ] + }, + "User2faRecoveryCodesResponse": { + "type": "object", + "properties": { + "recoveryCodes": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-f0-9]{32}$" + } + } + }, + "required": [ + "recoveryCodes" + ] + }, + "User2faEnableFinishRequest": { + "type": "object", + "properties": { + "loginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "authenticatorToken": { + "type": "string", + "pattern": "^\\d{6}$" + } + }, + "required": [ + "loginHash", + "authenticatorToken" + ] + }, + "EmailVerificationResendRequest": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + } + }, + "required": [ + "email" + ] + }, + "EmailVerificationConfirmRequest": { + "type": "object", + "properties": { + "emailVerificationCode": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9_-]{21}$" + } + }, + "required": [ + "emailVerificationCode" + ] + }, + "SessionRefreshSuccess": { + "type": "object", + "properties": { + "oldSessionKey": { + "type": "string", + "format": "byte" + }, + "newSessionKey": { + "type": "string", + "format": "byte" + } + }, + "required": [ + "oldSessionKey", + "newSessionKey" + ] + }, + "StripeCheckoutSessionResponse": { + "type": "object", + "properties": { + "checkoutSessionUrl": { + "type": "string", + "format": "uri", + "example": "https://checkout.stripe.com/c/pay/..." + } + }, + "required": [ + "checkoutSessionUrl" + ] + }, + "StripeCheckoutSessionRequest": { + "type": "object", + "properties": { + "billingFrequency": { + "type": "string", + "enum": [ + "monthly", + "yearly" + ], + "description": "Defaults to `monthly` when omitted (legacy tRPC)." + } + } + }, + "StripePortalSessionResponse": { + "type": "object", + "properties": { + "portalSessionUrl": { + "type": "string", + "format": "uri", + "example": "https://billing.stripe.com/p/session/..." + } + }, + "required": [ + "portalSessionUrl" + ] + } + }, + "parameters": {} + }, + "paths": { + "/api/health": { + "get": { + "summary": "Health check", + "responses": { + "200": { + "description": "API is reachable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } + } + } + } + } + }, + "/api/sessions/login": { + "post": { + "summary": "Create session (email + login hash)", + "description": "Replaces legacy `sessions.login`. Sets httpOnly cookies when implemented.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionLoginRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Login succeeded; cookies set.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionLoginSuccess" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "429": { + "description": "Too many failed login attempts (rate limited).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users": { + "post": { + "summary": "Register a new account", + "description": "Replaces legacy `users.account.register`. Creates user, personal group, and first page; sets email verification unless `SEND_EMAILS=false` (then verifies immediately, legacy parity).", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserRegisterRequest" + } + } + } + }, + "responses": { + "201": { + "description": "User created. `emailVerified` is true when outbound mail is disabled (`SEND_EMAILS=false`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserRegisterResponse" + } + } + } + }, + "400": { + "description": "Validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "409": { + "description": "Resource already exists (e.g. email already registered).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "502": { + "description": "Email send failed (e.g. Resend API error after user row was created; rare).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me": { + "get": { + "summary": "Current user (from access cookie)", + "description": "Minimal account summary for the authenticated user (`accessToken` cookie).", + "responses": { + "200": { + "description": "Authenticated user.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserMeResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "delete": { + "summary": "Delete current account (password confirmation)", + "description": "Replaces legacy `users.account.delete`. Requires `accessToken` cookie and correct `loginHash` in the JSON body. Clears session cookies on success. Optional Stripe customer deletion is handled by the deployment (not part of OpenAPI).", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserAccountDeleteRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Account removed; session cookies cleared (same names as login)." + }, + "400": { + "description": "Wrong password, ownership constraint, or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/{userId}/public-keyring": { + "get": { + "summary": "User public keyring (E2EE box key)", + "description": "Returns `users.public_keyring` for wrapping group secrets when sending invitations. Any authenticated user may read (same visibility as legacy KeyDB `user:*:public-keyring`).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "userId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Base64 libsodium public keyring bytes.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPublicKeyringResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/groups": { + "get": { + "summary": "List group IDs for the current user", + "description": "Replaces legacy `users.pages.getGroupIds`. Returns `group_id` values from `group_members` ordered by recent activity (desc).", + "responses": { + "200": { + "description": "Ordered group ids.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserGroupIdsResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/pages/starting": { + "get": { + "summary": "Starting page id for the current user", + "description": "Replaces legacy `users.pages.getStartingPageId` (reads `users.starting_page_id`).", + "responses": { + "200": { + "description": "Nanoid of the userโ€™s starting page.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserStartingPageResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/pages/path": { + "get": { + "summary": "Breadcrumb path from a page to the personal main page", + "description": "Replaces legacy `users.pages.getCurrentPath`. Uses `users_pages.last_parent_id` and may repair a missing parent link once (legacy KeyDB behavior).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "Page to resolve toward the personal group main page.", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "initialPageId", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Ordered page ids from root (personal main) to `initialPageId`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserCurrentPathResponse" + } + } + } + }, + "400": { + "description": "Missing or invalid `initialPageId` query parameter.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/pages/recent": { + "get": { + "summary": "Recent page ids for the current user", + "description": "Returns `users.recent_page_ids` (most recently bumped first). Complements POST mutations that do not echo the list.", + "responses": { + "200": { + "description": "Ordered recent page nanoids.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPageIdListResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/pages/favorites": { + "get": { + "summary": "Favorite page ids for the current user", + "description": "Returns `users.favorite_page_ids` (newest additions first per add order). Complements POST mutations.", + "responses": { + "200": { + "description": "Ordered favorite page nanoids.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPageIdListResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "post": { + "summary": "Add favorite pages", + "description": "Replaces legacy `users.pages.addFavoritePages`. Favorites are stored in Postgres (`users.favorite_page_ids`); legacy used KeyDB only.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPageIdsBody" + } + } + } + }, + "responses": { + "204": { + "description": "Favorites merged (order: new ids first, then existing)." + }, + "400": { + "description": "Validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/pages/recent/remove": { + "post": { + "summary": "Remove page ids from recent list", + "description": "Replaces legacy `users.pages.removeRecentPages`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPageIdsBody" + } + } + } + }, + "responses": { + "204": { + "description": "Updated `users.recent_page_ids`." + }, + "400": { + "description": "Validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/pages/recent/clear": { + "post": { + "summary": "Clear recent pages", + "description": "Replaces legacy `users.pages.clearRecentPages`.", + "responses": { + "204": { + "description": "Recent list emptied." + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/pages/favorites/remove": { + "post": { + "summary": "Remove favorite pages", + "description": "Replaces legacy `users.pages.removeFavoritePages`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPageIdsBody" + } + } + } + }, + "responses": { + "204": { + "description": "Favorites updated." + }, + "400": { + "description": "Validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/pages/favorites/clear": { + "post": { + "summary": "Clear favorite pages", + "description": "Replaces legacy `users.pages.clearFavoritePages`.", + "responses": { + "204": { + "description": "Favorites emptied." + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/defaults/note": { + "patch": { + "summary": "Update encrypted default note template", + "description": "Replaces legacy `users.pages.setEncryptedDefaultNote`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDefaultNotePatch" + } + } + } + }, + "responses": { + "204": { + "description": "`users.encrypted_default_note` updated." + }, + "400": { + "description": "Validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/defaults/arrow": { + "patch": { + "summary": "Update encrypted default arrow template", + "description": "Replaces legacy `users.pages.setEncryptedDefaultArrow`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDefaultArrowPatch" + } + } + } + }, + "responses": { + "204": { + "description": "`users.encrypted_default_arrow` updated." + }, + "400": { + "description": "Validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/notifications": { + "get": { + "summary": "Load notifications for the current user", + "description": "Replaces legacy `users.pages.notifications.load`. Ciphertext fields are base64 in JSON.", + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "description": "Return notifications strictly older than this id (legacy pagination)." + }, + "required": false, + "name": "lastNotificationId", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Window of notifications and optional `lastNotificationRead`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserNotificationsLoadResponse" + } + } + } + }, + "400": { + "description": "Invalid query parameters.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/notifications/read": { + "post": { + "summary": "Mark all notifications as read", + "description": "Replaces legacy `users.pages.notifications.markAsRead`. Sets `users.last_notification_read` to the latest linked notification id.", + "responses": { + "204": { + "description": "Read cursor updated (no-op if user has no notifications)." + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/main-page": { + "get": { + "summary": "Get the group main page id", + "description": "Replaces legacy `groups.getMainPageId` (KeyDB `main-page-id`). Source: `groups.main_page_id`. Requires `viewGroupPages` (same as listing pages).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Main page id for the group.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupMainPageResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/members": { + "get": { + "summary": "List user ids (members, requests, invitations)", + "description": "Replaces legacy `groups.getUserIds`: union of `group_members`, `group_join_requests`, and `group_join_invitations` for the group. Requires `viewGroupMembers` (not granted for public read without membership).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Distinct user ids (unordered).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupMemberUserIdsResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/members/detail": { + "get": { + "summary": "Group membership detail (roles + pending invites/requests)", + "description": "Returns the callerโ€™s role, full member list with roles, pending invitations and non-rejected join requests, and group flags (`groupIsPublic`, `joinRequestsAllowed`). Requires `viewGroupMembers` and an active membership row.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Structured membership for admin UIs.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupMembersDetailResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/invite-crypto-bootstrap": { + "get": { + "summary": "Encrypted group key material for invitation flows (managers)", + "description": "Returns ciphertext the callerโ€™s browser unwraps to build `POST โ€ฆ/join-invitations` and `POST โ€ฆ/join-requests/{userId}/accept` bodies. Requires membership with manager role (owner/admin/moderator). With optional `inviteeUserId` query, also returns public keyrings for E2EE group notifications (legacy WS step 2).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "When set, includes public keyrings for group managers and this user so the SPA can build `notifications` on `POST โ€ฆ/join-invitations`.", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": false, + "name": "inviteeUserId", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Encrypted keyrings + group public key.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupInviteCryptoBootstrapResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/collab-crypto-context": { + "get": { + "summary": "Group ciphertext for page move / re-key (edit pages)", + "description": "Returns `groups.encrypted_content_keyring`, optional `access_keyring`, and the callerโ€™s `group_members.encrypted_access_keyring` so the SPA can unwrap `GroupContentKeyring` when moving a page into this group. Requires `editGroupPages` and membership.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Encrypted blobs for destination-side symmetric wrap.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupCollabCryptoContextResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/privacy/make-private-bootstrap": { + "get": { + "summary": "Read model for making a public group private (re-key)", + "description": "Returns member/invite/request/page ciphertext ids and user public keys matching legacy WS `groups.privacy.makePrivate` step 1, so the browser can build `POST โ€ฆ/privacy/private`. Requires Pro, `editGroupSettings`, and a public group (`access_keyring` set).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Key rotation bootstrap (base64 fields).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPrivacyMakePrivateBootstrapResponse" + } + } + } + }, + "400": { + "description": "Group is already private.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/public-keyring": { + "get": { + "summary": "Group public keyring for name encryption", + "description": "Returns `groups.public_keyring` when the caller may encrypt a display name for invite accept or join-request flows: active member, pending invitation, or join requests allowed and not yet a member.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Base64 group box public keyring.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPublicKeyringResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/pages": { + "get": { + "summary": "List page IDs in a group", + "description": "Replaces legacy `groups.getPages` (`optionalAuthProcedure`). Cookie optional: **public** groups (`access_keyring` set) may be listed without a session (same as legacy anonymous `viewGroupPages`). Private groups require `viewGroupPages`. Optional `lastPageId` cursor for pagination (newest `last_activity_date` first). Omits soft-deleted pages (`permanent_deletion_date` set).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "Pagination cursor: return pages older than this page's activity (legacy `lastPageId`)." + }, + "required": false, + "name": "lastPageId", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Page id window (max 20) and `hasMore`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPagesListResponse" + } + } + } + }, + "400": { + "description": "Invalid `lastPageId` (not in group).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "post": { + "summary": "Create a page in a group", + "description": "Replaces legacy `pages.create`. For an **existing** group, `parentPageId` must be a page in that group and the caller needs `editGroupPages`. With optional `groupCreation`, path `groupId` is a **new** nanoid (no row yet), `parentPageId` is a page in the userโ€™s **personal** group, and the body includes the same ciphertext as `PageMoveGroupCreationRequest` โ€” Pro only; creates the `groups` + owner `group_members` rows then the first page (legacy parity). The 50 free-page cap applies to nonโ€‘Pro users for normal creates.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPageCreateRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Page and `users_pages` row created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPageCreateResponse" + } + } + } + }, + "400": { + "description": "Invalid parent page or body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/password": { + "post": { + "summary": "Enable group password (Pro)", + "description": "Replaces `groups.password.enable`. Argon2id is applied on the server to the provided `groupPasswordHash` material (base64) and stored encrypted. Requires `editGroupSettings` and a Pro plan.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPasswordEnableRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Password protection enabled." + }, + "400": { + "description": "Already protected or bad password material.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "patch": { + "summary": "Change group password (Pro)", + "description": "Replaces `groups.password.change`. Verifies the current group password, then re-wraps the content keyring.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPasswordChangeRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Password updated." + }, + "400": { + "description": "Wrong password, or group not protected.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "delete": { + "summary": "Disable group password (not Pro check in legacy for disable-only)", + "description": "Replaces `groups.password.disable`. Verifies the current group password, removes server-side group password, updates `groupEncryptedContentKeyring`.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPasswordDisableRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Password protection disabled." + }, + "400": { + "description": "Wrong password, or not protected.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/privacy/public": { + "post": { + "summary": "Make group public (Pro)", + "description": "Replaces `groups.privacy.makePublic`. Sets `access_keyring` and clears member/invite `encrypted_access_keyring`.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPrivacyPublicRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Group is public." + }, + "400": { + "description": "Already public.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/privacy/join-requests": { + "patch": { + "summary": "Allow or reject join requests (Pro)", + "description": "Replaces `groups.privacy.setJoinRequestsAllowed`.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPrivacyJoinRequestsPatch" + } + } + } + }, + "responses": { + "204": { + "description": "Setting updated." + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/privacy/private": { + "post": { + "summary": "Make group private (Pro) โ€” full re-key payload", + "description": "Replaces legacy WS `groups.privacy.makePrivate` (step 2 `rotateGroupKeys`) in one request. Clears `access_keyring` when `groupAccessKeyring` is omitted. Member / invitation / request / page record keys must match the DB exactly.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPrivacyPrivateRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Group is private; ciphertext updated." + }, + "400": { + "description": "Already private or payload key sets do not match group.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}": { + "delete": { + "summary": "Soft-delete group (grace period)", + "description": "Replaces `groups.deletion.delete`. Sets `permanent_deletion_date` ~1 month ahead.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Deletion scheduled." + }, + "400": { + "description": "Already soft-deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/restore": { + "post": { + "summary": "Restore a soft-deleted group", + "description": "Replaces `groups.deletion.restore` during the grace period (`permanent_deletion_date` in the future).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Group removed from scheduled deletion." + }, + "400": { + "description": "Not soft-deleted, or no longer in grace period.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/purge": { + "post": { + "summary": "Permanently mark group deleted (purge active or grace state)", + "description": "Replaces `groups.deletion.deletePermanently` โ€” `permanent_deletion_date` set in the past (legacy).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Purge recorded." + }, + "400": { + "description": "Already purged.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/join-invitations": { + "post": { + "summary": "Send a group join invitation (Pro)", + "description": "Replaces legacy WS `groups.joinInvitations.send` step 1. Deletes a conflicting join request for the invitee. For private groups, `encryptedAccessKeyring` is required; for public groups it is stored as null.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupJoinInvitationSendRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Invitation created." + }, + "400": { + "description": "Already invited, already a member, or missing keyring for private group.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/join-invitations/me/accept": { + "post": { + "summary": "Accept a pending join invitation (Pro)", + "description": "Replaces legacy WS `groups.joinInvitations.accept` step 1.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupJoinInvitationAcceptRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Invitation consumed; user added to `group_members`." + }, + "400": { + "description": "Validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/join-invitations/me/reject": { + "post": { + "summary": "Reject a pending join invitation", + "description": "Replaces legacy WS `groups.joinInvitations.reject` step 1 (no Pro check in legacy).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Invitation removed." + }, + "400": { + "description": "No pending invitation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/join-invitations/{userId}": { + "delete": { + "summary": "Cancel a join invitation (Pro)", + "description": "Replaces legacy WS `groups.joinInvitations.cancel` step 1. Path `userId` is the invitee. Requires permission to manage the invited role.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "userId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Invitation removed." + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/join-requests": { + "post": { + "summary": "Send a join request (Pro)", + "description": "Replaces legacy WS `groups.joinRequests.send` step 1. Requires `are_join_requests_allowed` on the group.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupJoinRequestSendRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Join request created." + }, + "400": { + "description": "Already pending.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/join-requests/{userId}/accept": { + "post": { + "summary": "Accept a join request (Pro)", + "description": "Replaces legacy WS `groups.joinRequests.accept` step 1. Path `userId` is the requester.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "userId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupJoinRequestAcceptRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Requester added to `group_members`." + }, + "400": { + "description": "No pending request or missing access keyring for private group.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/join-requests/{userId}/reject": { + "post": { + "summary": "Reject a join request (Pro)", + "description": "Replaces legacy WS `groups.joinRequests.reject` step 1. Sets `rejected` on the request (legacy does not delete the row).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "userId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Request marked rejected." + }, + "400": { + "description": "No pending request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/join-requests/me/cancel": { + "post": { + "summary": "Cancel own join request (Pro)", + "description": "Replaces legacy WS `groups.joinRequests.cancel` step 1.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Join request row deleted." + }, + "400": { + "description": "No pending request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/members/{userId}": { + "patch": { + "summary": "Change a member's role (Pro)", + "description": "Replaces legacy WS `groups.changeUserRole` step 1.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "userId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupMemberRolePatchRequest" + } + } + } + }, + "responses": { + "204": { + "description": "`group_members.role` updated." + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "delete": { + "summary": "Remove a member (or leave)", + "description": "Replaces legacy WS `groups.removeUser` step 1. Callers may remove themselves without `canManageRole` on others.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "userId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Membership removed." + }, + "400": { + "description": "Cannot remove the last owner.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/move": { + "post": { + "summary": "Move page (optionally create group, re-key, set main)", + "description": "Replaces `websocket/pages/move` โ€” Pro-only; `editGroupSettings` on the page's current group, `editGroupPages` on destination unless `groupCreation` creates it. `reencrypt` is required when the page changes group (Yjs `page_updates` replaced with a single index-0 row; snapshots updated by id).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageMoveRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Move completed." + }, + "400": { + "description": "No-op move, or invalid payload.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/bump": { + "post": { + "summary": "Bump page (recents, activity, optional breadcrumb parent)", + "description": "Replaces `pages.bump` โ€” `users` starting + recents, optional `users_pages.last_parent_id` when the parent chain ends at the personal main page (`lastParentId` walk).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageBumpRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Bumped (best-effort; loop in chain exits without updating parent)." + }, + "400": { + "description": "Invalid parent (chain does not resolve to main page).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/collab-updates": { + "get": { + "summary": "List encrypted Yjs page updates (Postgres)", + "description": "Bootstrap for the editor: returns `page_updates` rows for the page, ordered by `index`. Use `sinceIndex` to paginate incrementally (default limit 100, max 500). Does not use legacy Redis collab cache โ€” Postgres only. Full duplex collab remains a separate WebSocket track (Phase 3).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "If provided, only returns updates with `index > sinceIndex` (exclusive). Used for incremental bootstrap after the first batch." + }, + "required": false, + "name": "sinceIndex", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Max updates to return per request (default 100, max 500)." + }, + "required": false, + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Current ciphertext chain.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageCollabUpdatesGetResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "post": { + "summary": "Append page updates (optimistic concurrency)", + "description": "Appends ciphertext rows to `page_updates`. `expectedLastIndex` must match the current max index (or null when empty). **409** when another writer advanced the chain โ€” client should re-GET and retry.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageCollabUpdatesAppendRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Updates persisted." + }, + "400": { + "description": "Bad index sequence or wrong `expectedLastIndex` for an empty page.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "409": { + "description": "Stale `expectedLastIndex` (concurrent append).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/backlinks": { + "post": { + "summary": "Create page backlink (source โ†’ this page as target)", + "description": "Replaces `pages.backlinks.create`. Path `pageId` is the **target**; body has `sourcePageId`.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageBacklinkCreateRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Backlink created or activity updated (upsert)." + }, + "400": { + "description": "Source and target identical.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "get": { + "summary": "List backlinks pointing to this page", + "description": "Returns source page IDs that link to this page, ordered by most recent activity.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Backlink source page IDs.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageBacklinkListResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/backlinks/{targetPageId}": { + "delete": { + "summary": "Delete backlink from source page to target page", + "description": "Replaces `pages.backlinks.delete`. Path `pageId` is **source**; `targetPageId` is the link target (legacy input names).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "targetPageId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Backlink removed." + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/snapshots": { + "get": { + "summary": "List page snapshot metadata (Pro)", + "description": "Returns snapshot ids and timestamps for manual and pre-restore snapshots (no ciphertext).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Newest first.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageSnapshotListResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "post": { + "summary": "Save encrypted page snapshot (Pro)", + "description": "Replaces `pages.snapshots.save` โ€” asserts Pro plan (legacy `assertUserSubscribed`).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageSnapshotSaveRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Snapshot id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageSnapshotCreateResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/snapshots/{snapshotId}": { + "get": { + "summary": "Load page snapshot ciphertext (Pro)", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "snapshotId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Ciphertext (base64 fields).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageSnapshotLoadResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "delete": { + "summary": "Delete a page snapshot", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "snapshotId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Snapshot removed." + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}": { + "delete": { + "summary": "Soft-delete page (grace period)", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Deletion scheduled (not main page)." + }, + "400": { + "description": "Already deleted, or is group main page.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/restore": { + "post": { + "summary": "Restore a soft-deleted page", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Page restored in grace." + }, + "400": { + "description": "Not deleted, or free page past purge date.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/purge": { + "post": { + "summary": "Permanently mark page deleted; refunds free page when applicable", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Purge recorded." + }, + "400": { + "description": "Is main page, or already purged.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/password": { + "post": { + "summary": "Change password (re-wrap keyrings)", + "description": "Replaces legacy WebSocket `users.account.changePassword`. Requires `accessToken`; verifies `oldLoginHash`; stores keyrings encrypted with the new password (`userEncrypted*` are plaintext keyrings from the client, same as registration). Invalidates all sessions and clears cookies โ€” client must log in again.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPasswordChangeRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Password updated; all sessions invalidated; session cookies cleared." + }, + "400": { + "description": "Wrong current password or invalid key material.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/email-change": { + "post": { + "summary": "Request account email change (6-digit code email)", + "description": "Replaces legacy `users.account.emailChange.request`. Verifies `oldLoginHash` and that the new address is not already registered. When outbound email is enabled, sends a 6-digit code. When `SEND_EMAILS=false` (e.g. local), returns 200 with `emailVerificationCode` instead of emailing.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserEmailChangeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Out-of-band dev response when `SEND_EMAILS=false` (verification code not emailed).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserEmailChangeRequestResponse" + } + } + } + }, + "204": { + "description": "Code emailed to the new address; pending change stored on the user row." + }, + "400": { + "description": "Wrong password, address in use, or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "502": { + "description": "Email send failed (e.g. Resend) after the pending state was written.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/email-change/confirm": { + "post": { + "summary": "Confirm email change (re-wrap keyrings, new password)", + "description": "Replaces legacy WebSocket `users.account.emailChange.finish` (step 1 + 2 in one). Verifies 6-digit code and `oldLoginHash`, then applies new email + new password-encrypted keyrings, invalidates sessions, clears cookies; optional Stripe customer email update in the deployment (not in OpenAPI).", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserEmailChangeConfirmRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Email updated; sessions cleared; re-login required." + }, + "400": { + "description": "Wrong code, wrong password, no pending change, or invalid keyrings.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/2fa/enable/request": { + "post": { + "summary": "Start 2FA setup (TOTP secret + otpauth URI)", + "description": "Replaces `users.account.twoFactorAuth.enable.request`. Stores a pending encrypted authenticator secret; client shows QR from `keyUri` or `secret`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faPasswordBody" + } + } + } + }, + "responses": { + "200": { + "description": "Secret generated; not yet enabled until `โ€ฆ/enable/finish`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faEnableRequestResponse" + } + } + } + }, + "400": { + "description": "Validation error, or 2FA already fully enabled.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/2fa/enable/finish": { + "post": { + "summary": "Complete 2FA setup (TOTP + recovery codes)", + "description": "Replaces `users.account.twoFactorAuth.enable.finish`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faEnableFinishRequest" + } + } + } + }, + "responses": { + "200": { + "description": "2FA enabled; one-time recovery codes returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faRecoveryCodesResponse" + } + } + } + }, + "400": { + "description": "Wrong password, wrong TOTP, or already enabled.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/2fa/load": { + "post": { + "summary": "Reveal TOTP secret and otpauth URI (after password check)", + "description": "Replaces `users.account.twoFactorAuth.load` (legacy tRPC had `loginHash` in the query; this API uses a JSON body on POST to avoid putting secrets in query strings or logs).", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faPasswordBody" + } + } + } + }, + "responses": { + "200": { + "description": "Secret and `keyUri` for re-provisioning an authenticator.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faEnableRequestResponse" + } + } + } + }, + "400": { + "description": "Wrong password or 2FA not enabled.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/2fa/recovery-codes": { + "post": { + "summary": "Regenerate recovery codes", + "description": "Replaces `users.account.twoFactorAuth.generateRecoveryCodes`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faPasswordBody" + } + } + } + }, + "responses": { + "200": { + "description": "New recovery codes (previous codes invalidated).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faRecoveryCodesResponse" + } + } + } + }, + "400": { + "description": "Wrong password or 2FA not enabled.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/2fa/devices/forget": { + "post": { + "summary": "Mark all user devices as not trusted", + "description": "Replaces `users.account.twoFactorAuth.forgetTrustedDevices`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faPasswordBody" + } + } + } + }, + "responses": { + "204": { + "description": "`devices.trusted` cleared for this user." + }, + "400": { + "description": "Wrong password or 2FA not enabled.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/2fa/disable": { + "post": { + "summary": "Disable 2FA", + "description": "Replaces `users.account.twoFactorAuth.disable`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faPasswordBody" + } + } + } + }, + "responses": { + "204": { + "description": "2FA disabled; authenticator and recovery material cleared." + }, + "400": { + "description": "Wrong password or 2FA not enabled.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/email-verification/resend": { + "post": { + "summary": "Resend email verification (public, by email)", + "description": "Replaces legacy `users.account.resendVerificationEmail`. Uses Resend when `SEND_EMAILS` is not `false`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailVerificationResendRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Email sent (or accepted by provider)." + }, + "400": { + "description": "Validation error or outbound email disabled for this environment.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "409": { + "description": "Resource already exists (e.g. email already registered).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "502": { + "description": "Email provider (Resend) request failed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/email-verification/confirm": { + "post": { + "summary": "Confirm email with nanoid code", + "description": "Replaces legacy `users.account.verifyEmail` (public).", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailVerificationConfirmRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Email verified; account updated." + }, + "400": { + "description": "Invalid or expired code.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + } + } + } + }, + "/api/sessions/refresh": { + "post": { + "summary": "Rotate access token using refresh cookie", + "description": "Replaces legacy `sessions.refresh`.", + "responses": { + "200": { + "description": "New session key and cookies.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionRefreshSuccess" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/sessions/logout": { + "post": { + "summary": "Invalidate session and clear cookies", + "description": "Replaces legacy `sessions.logout`.", + "responses": { + "204": { + "description": "Logged out (cookies cleared)." + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/sessions/demo": { + "post": { + "summary": "Create demo user and session", + "description": "Replaces legacy `sessions.startDemo`. Request body will match registration key material once defined.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionDemoRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Demo user created; same response shape as login.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionLoginSuccess" + } + } + } + }, + "400": { + "description": "Validation error (e.g. unsupported group password on demo).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/billing/stripe/checkout-session": { + "post": { + "summary": "Create Stripe Checkout (subscription) session", + "description": "Replaces legacy `users.account.stripe.createCheckoutSession`. Resolves or creates a Stripe customer from `users.customer_id` and decrypted account email, then returns a hosted Checkout URL. Requires verified email. Demo accounts receive **403**.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StripeCheckoutSessionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Checkout session URL (hosted Stripe page).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StripeCheckoutSessionResponse" + } + } + } + }, + "400": { + "description": "Already subscribed, or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/billing/stripe/portal-session": { + "post": { + "summary": "Create Stripe Customer Portal session", + "description": "Replaces legacy `users.account.stripe.createPortalSession`. Requires a `users.customer_id` (create checkout first or migrate from legacy).", + "responses": { + "200": { + "description": "Portal session URL.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StripePortalSessionResponse" + } + } + } + }, + "400": { + "description": "No Stripe customer on file.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/webhooks/stripe": { + "post": { + "summary": "Stripe webhooks (signed raw body)", + "description": "Replaces the legacy Fastify `POST /stripe/webhook` handler. Send the **raw** request body; verification uses the `Stripe-Signature` header and `STRIPE_WEBHOOK_SECRET`. Handles `customer.subscription.updated` and `customer.subscription.deleted` by updating `users.plan` and `users.subscription_id` via `users.customer_id`.", + "responses": { + "200": { + "description": "Event acknowledged." + }, + "400": { + "description": "Missing or invalid signature.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + } + } +} diff --git a/new-deepnotes/apps/web/src/app.test.ts b/new-deepnotes/apps/web/src/app.test.ts new file mode 100644 index 00000000..920a8b7d --- /dev/null +++ b/new-deepnotes/apps/web/src/app.test.ts @@ -0,0 +1,29 @@ +import { mount, flushPromises } from "@vue/test-utils"; +import { nextTick } from "vue"; +import { describe, expect, it } from "vitest"; + +import App from "./App.vue"; +import { createAppRouter } from "./router"; + +describe("App", () => { + it("renders shell after session bootstrap (no session cookie)", async () => { + const router = createAppRouter(); + await router.push("/"); + await router.isReady(); + + const wrapper = mount(App, { + global: { plugins: [router] }, + }); + for (let i = 0; i < 30; i++) { + await flushPromises(); + await nextTick(); + if (wrapper.find('[data-testid="app-shell"]').exists()) { + break; + } + } + + expect(wrapper.find('[data-testid="app-shell"]').exists()).toBe(true); + expect(wrapper.text()).toContain("DeepNotes"); + expect(wrapper.text()).toContain("Sign in"); + }); +}); diff --git a/new-deepnotes/apps/web/src/components/ColorPalette.vue b/new-deepnotes/apps/web/src/components/ColorPalette.vue new file mode 100644 index 00000000..fb8496df --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ColorPalette.vue @@ -0,0 +1,178 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/Footer.vue b/new-deepnotes/apps/web/src/components/Footer.vue new file mode 100644 index 00000000..a278b027 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/Footer.vue @@ -0,0 +1,54 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/NavBar.vue b/new-deepnotes/apps/web/src/components/NavBar.vue new file mode 100644 index 00000000..3dfbd35f --- /dev/null +++ b/new-deepnotes/apps/web/src/components/NavBar.vue @@ -0,0 +1,93 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/alert/Alert.vue b/new-deepnotes/apps/web/src/components/ui/alert/Alert.vue new file mode 100644 index 00000000..916295ba --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/alert/Alert.vue @@ -0,0 +1,21 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/alert/AlertAction.vue b/new-deepnotes/apps/web/src/components/ui/alert/AlertAction.vue new file mode 100644 index 00000000..8fd2f1ec --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/alert/AlertAction.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/alert/AlertDescription.vue b/new-deepnotes/apps/web/src/components/ui/alert/AlertDescription.vue new file mode 100644 index 00000000..43b45a9a --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/alert/AlertDescription.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/alert/AlertTitle.vue b/new-deepnotes/apps/web/src/components/ui/alert/AlertTitle.vue new file mode 100644 index 00000000..b9694a62 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/alert/AlertTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/alert/index.ts b/new-deepnotes/apps/web/src/components/ui/alert/index.ts new file mode 100644 index 00000000..4c797da0 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/alert/index.ts @@ -0,0 +1,21 @@ +import type { VariantProps } from 'class-variance-authority' +import { cva } from 'class-variance-authority' + +export { default as Alert } from './Alert.vue' +export { default as AlertAction } from './AlertAction.vue' +export { default as AlertDescription } from './AlertDescription.vue' +export { default as AlertTitle } from './AlertTitle.vue' + +export const alertVariants = cva('grid gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*=size-])]:size-4 group/alert relative w-full', { + variants: { + variant: { + default: 'bg-card text-card-foreground', + destructive: 'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current', + }, + }, + defaultVariants: { + variant: 'default', + }, +}) + +export type AlertVariants = VariantProps diff --git a/new-deepnotes/apps/web/src/components/ui/button/Button.vue b/new-deepnotes/apps/web/src/components/ui/button/Button.vue new file mode 100644 index 00000000..1b6a5120 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/button/Button.vue @@ -0,0 +1,31 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/button/index.ts b/new-deepnotes/apps/web/src/components/ui/button/index.ts new file mode 100644 index 00000000..676a977f --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/button/index.ts @@ -0,0 +1,35 @@ +import type { VariantProps } from 'class-variance-authority' +import { cva } from 'class-variance-authority' + +export { default as Button } from './Button.vue' + +export const buttonVariants = cva( + 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*=size-])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80', + outline: 'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground', + ghost: 'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground', + destructive: 'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + 'default': 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', + 'xs': 'h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*=size-])]:size-3', + 'sm': 'h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*=size-])]:size-3.5', + 'lg': 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', + 'icon': 'size-8', + 'icon-xs': 'size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*=size-])]:size-3', + 'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg', + 'icon-lg': 'size-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +) +export type ButtonVariants = VariantProps diff --git a/new-deepnotes/apps/web/src/components/ui/card/Card.vue b/new-deepnotes/apps/web/src/components/ui/card/Card.vue new file mode 100644 index 00000000..073e6516 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/card/Card.vue @@ -0,0 +1,21 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/card/CardAction.vue b/new-deepnotes/apps/web/src/components/ui/card/CardAction.vue new file mode 100644 index 00000000..c2beb206 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/card/CardAction.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/card/CardContent.vue b/new-deepnotes/apps/web/src/components/ui/card/CardContent.vue new file mode 100644 index 00000000..6270bc46 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/card/CardContent.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/card/CardDescription.vue b/new-deepnotes/apps/web/src/components/ui/card/CardDescription.vue new file mode 100644 index 00000000..722b2033 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/card/CardDescription.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/card/CardFooter.vue b/new-deepnotes/apps/web/src/components/ui/card/CardFooter.vue new file mode 100644 index 00000000..ca3936b4 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/card/CardFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/card/CardHeader.vue b/new-deepnotes/apps/web/src/components/ui/card/CardHeader.vue new file mode 100644 index 00000000..27d56f7e --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/card/CardHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/card/CardTitle.vue b/new-deepnotes/apps/web/src/components/ui/card/CardTitle.vue new file mode 100644 index 00000000..1f53990e --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/card/CardTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/card/index.ts b/new-deepnotes/apps/web/src/components/ui/card/index.ts new file mode 100644 index 00000000..73d985f2 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/card/index.ts @@ -0,0 +1,7 @@ +export { default as Card } from './Card.vue' +export { default as CardAction } from './CardAction.vue' +export { default as CardContent } from './CardContent.vue' +export { default as CardDescription } from './CardDescription.vue' +export { default as CardFooter } from './CardFooter.vue' +export { default as CardHeader } from './CardHeader.vue' +export { default as CardTitle } from './CardTitle.vue' diff --git a/new-deepnotes/apps/web/src/components/ui/checkbox/Checkbox.vue b/new-deepnotes/apps/web/src/components/ui/checkbox/Checkbox.vue new file mode 100644 index 00000000..4f62b48e --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/checkbox/Checkbox.vue @@ -0,0 +1,34 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/checkbox/index.ts b/new-deepnotes/apps/web/src/components/ui/checkbox/index.ts new file mode 100644 index 00000000..8c28c286 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/checkbox/index.ts @@ -0,0 +1 @@ +export { default as Checkbox } from './Checkbox.vue' diff --git a/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenu.vue b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenu.vue new file mode 100644 index 00000000..c08018ba --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenu.vue @@ -0,0 +1,19 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue new file mode 100644 index 00000000..921fc589 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue @@ -0,0 +1,43 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuContent.vue b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuContent.vue new file mode 100644 index 00000000..be50a887 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuContent.vue @@ -0,0 +1,40 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuGroup.vue b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuGroup.vue new file mode 100644 index 00000000..ab71e1ad --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuGroup.vue @@ -0,0 +1,15 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuItem.vue b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuItem.vue new file mode 100644 index 00000000..b6088984 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuItem.vue @@ -0,0 +1,31 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuLabel.vue b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuLabel.vue new file mode 100644 index 00000000..37d96f01 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuLabel.vue @@ -0,0 +1,23 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue new file mode 100644 index 00000000..8306812c --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue @@ -0,0 +1,21 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue new file mode 100644 index 00000000..416e5ba8 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue @@ -0,0 +1,44 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue new file mode 100644 index 00000000..e90bcc8a --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue @@ -0,0 +1,23 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue new file mode 100644 index 00000000..7bfa823a --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuSub.vue b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuSub.vue new file mode 100644 index 00000000..ef54276d --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuSub.vue @@ -0,0 +1,18 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue new file mode 100644 index 00000000..d7837e26 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue @@ -0,0 +1,27 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue new file mode 100644 index 00000000..6005c31b --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue @@ -0,0 +1,32 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue new file mode 100644 index 00000000..39be3ff3 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/dropdown-menu/index.ts b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/index.ts new file mode 100644 index 00000000..f488d396 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/dropdown-menu/index.ts @@ -0,0 +1,16 @@ +export { default as DropdownMenu } from './DropdownMenu.vue' + +export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue' +export { default as DropdownMenuContent } from './DropdownMenuContent.vue' +export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue' +export { default as DropdownMenuItem } from './DropdownMenuItem.vue' +export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue' +export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue' +export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue' +export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue' +export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue' +export { default as DropdownMenuSub } from './DropdownMenuSub.vue' +export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue' +export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue' +export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue' +export { DropdownMenuPortal } from 'reka-ui' diff --git a/new-deepnotes/apps/web/src/components/ui/input/Input.vue b/new-deepnotes/apps/web/src/components/ui/input/Input.vue new file mode 100644 index 00000000..4ebb6aba --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/input/Input.vue @@ -0,0 +1,31 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/input/index.ts b/new-deepnotes/apps/web/src/components/ui/input/index.ts new file mode 100644 index 00000000..a691dd6c --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/input/index.ts @@ -0,0 +1 @@ +export { default as Input } from './Input.vue' diff --git a/new-deepnotes/apps/web/src/components/ui/label/Label.vue b/new-deepnotes/apps/web/src/components/ui/label/Label.vue new file mode 100644 index 00000000..9d30cbbe --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/label/Label.vue @@ -0,0 +1,26 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/label/index.ts b/new-deepnotes/apps/web/src/components/ui/label/index.ts new file mode 100644 index 00000000..572c2f01 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/label/index.ts @@ -0,0 +1 @@ +export { default as Label } from './Label.vue' diff --git a/new-deepnotes/apps/web/src/components/ui/select/Select.vue b/new-deepnotes/apps/web/src/components/ui/select/Select.vue new file mode 100644 index 00000000..553bf67f --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/select/Select.vue @@ -0,0 +1,19 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/select/SelectContent.vue b/new-deepnotes/apps/web/src/components/ui/select/SelectContent.vue new file mode 100644 index 00000000..e740d544 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/select/SelectContent.vue @@ -0,0 +1,58 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/select/SelectGroup.vue b/new-deepnotes/apps/web/src/components/ui/select/SelectGroup.vue new file mode 100644 index 00000000..4ae4b886 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/select/SelectGroup.vue @@ -0,0 +1,21 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/select/SelectItem.vue b/new-deepnotes/apps/web/src/components/ui/select/SelectItem.vue new file mode 100644 index 00000000..a66bb402 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/select/SelectItem.vue @@ -0,0 +1,45 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/select/SelectItemText.vue b/new-deepnotes/apps/web/src/components/ui/select/SelectItemText.vue new file mode 100644 index 00000000..af853947 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/select/SelectItemText.vue @@ -0,0 +1,15 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/select/SelectLabel.vue b/new-deepnotes/apps/web/src/components/ui/select/SelectLabel.vue new file mode 100644 index 00000000..42ae9958 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/select/SelectLabel.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/select/SelectScrollDownButton.vue b/new-deepnotes/apps/web/src/components/ui/select/SelectScrollDownButton.vue new file mode 100644 index 00000000..35a0fbe7 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/select/SelectScrollDownButton.vue @@ -0,0 +1,27 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/select/SelectScrollUpButton.vue b/new-deepnotes/apps/web/src/components/ui/select/SelectScrollUpButton.vue new file mode 100644 index 00000000..c6918749 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/select/SelectScrollUpButton.vue @@ -0,0 +1,27 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/select/SelectSeparator.vue b/new-deepnotes/apps/web/src/components/ui/select/SelectSeparator.vue new file mode 100644 index 00000000..17dadcfe --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/select/SelectSeparator.vue @@ -0,0 +1,19 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/select/SelectTrigger.vue b/new-deepnotes/apps/web/src/components/ui/select/SelectTrigger.vue new file mode 100644 index 00000000..f25c4f03 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/select/SelectTrigger.vue @@ -0,0 +1,34 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/select/SelectValue.vue b/new-deepnotes/apps/web/src/components/ui/select/SelectValue.vue new file mode 100644 index 00000000..c18762ee --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/select/SelectValue.vue @@ -0,0 +1,15 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/select/index.ts b/new-deepnotes/apps/web/src/components/ui/select/index.ts new file mode 100644 index 00000000..31b92946 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/select/index.ts @@ -0,0 +1,11 @@ +export { default as Select } from './Select.vue' +export { default as SelectContent } from './SelectContent.vue' +export { default as SelectGroup } from './SelectGroup.vue' +export { default as SelectItem } from './SelectItem.vue' +export { default as SelectItemText } from './SelectItemText.vue' +export { default as SelectLabel } from './SelectLabel.vue' +export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue' +export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue' +export { default as SelectSeparator } from './SelectSeparator.vue' +export { default as SelectTrigger } from './SelectTrigger.vue' +export { default as SelectValue } from './SelectValue.vue' diff --git a/new-deepnotes/apps/web/src/components/ui/switch/Switch.vue b/new-deepnotes/apps/web/src/components/ui/switch/Switch.vue new file mode 100644 index 00000000..19e69139 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/switch/Switch.vue @@ -0,0 +1,44 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/switch/index.ts b/new-deepnotes/apps/web/src/components/ui/switch/index.ts new file mode 100644 index 00000000..87b4b17d --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/switch/index.ts @@ -0,0 +1 @@ +export { default as Switch } from './Switch.vue' diff --git a/new-deepnotes/apps/web/src/features/account/AccountBilling.vue b/new-deepnotes/apps/web/src/features/account/AccountBilling.vue new file mode 100644 index 00000000..7901bb00 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/account/AccountBilling.vue @@ -0,0 +1,115 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/account/AccountGeneral.vue b/new-deepnotes/apps/web/src/features/account/AccountGeneral.vue new file mode 100644 index 00000000..50a502a2 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/account/AccountGeneral.vue @@ -0,0 +1,339 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/account/AccountLayout.vue b/new-deepnotes/apps/web/src/features/account/AccountLayout.vue new file mode 100644 index 00000000..3c552a9a --- /dev/null +++ b/new-deepnotes/apps/web/src/features/account/AccountLayout.vue @@ -0,0 +1,66 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/account/AccountSecurity.vue b/new-deepnotes/apps/web/src/features/account/AccountSecurity.vue new file mode 100644 index 00000000..f6aca51e --- /dev/null +++ b/new-deepnotes/apps/web/src/features/account/AccountSecurity.vue @@ -0,0 +1,371 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/account/AccountView.vue b/new-deepnotes/apps/web/src/features/account/AccountView.vue new file mode 100644 index 00000000..89a8fe51 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/account/AccountView.vue @@ -0,0 +1,829 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/account/account-routes.ts b/new-deepnotes/apps/web/src/features/account/account-routes.ts new file mode 100644 index 00000000..fc17d24b --- /dev/null +++ b/new-deepnotes/apps/web/src/features/account/account-routes.ts @@ -0,0 +1,29 @@ +import type { RouteRecordRaw } from "vue-router"; + +export const accountRoutes: RouteRecordRaw[] = [ + { + path: "/account", + component: () => import("./AccountLayout.vue"), + children: [ + { + path: "", + redirect: "/account/general", + }, + { + path: "general", + name: "account-general", + component: () => import("./AccountGeneral.vue"), + }, + { + path: "security", + name: "account-security", + component: () => import("./AccountSecurity.vue"), + }, + { + path: "billing", + name: "account-billing", + component: () => import("./AccountBilling.vue"), + }, + ], + }, +]; diff --git a/new-deepnotes/apps/web/src/features/auth/LoginView.vue b/new-deepnotes/apps/web/src/features/auth/LoginView.vue new file mode 100644 index 00000000..ea4e4b6b --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/LoginView.vue @@ -0,0 +1,231 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/auth/RegisterView.vue b/new-deepnotes/apps/web/src/features/auth/RegisterView.vue new file mode 100644 index 00000000..2b6d2951 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/RegisterView.vue @@ -0,0 +1,206 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/auth/auth-routes.ts b/new-deepnotes/apps/web/src/features/auth/auth-routes.ts new file mode 100644 index 00000000..6a0f0601 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/auth-routes.ts @@ -0,0 +1,16 @@ +import type { RouteRecordRaw } from "vue-router"; + +export const authRoutes: RouteRecordRaw[] = [ + { + path: "/login", + name: "login", + component: () => import("./LoginView.vue"), + meta: { public: true, layout: "auth" }, + }, + { + path: "/register", + name: "register", + component: () => import("./RegisterView.vue"), + meta: { public: true, layout: "auth" }, + }, +]; diff --git a/new-deepnotes/apps/web/src/features/auth/build-password-and-email-confirm.ts b/new-deepnotes/apps/web/src/features/auth/build-password-and-email-confirm.ts new file mode 100644 index 00000000..e11377ac --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/build-password-and-email-confirm.ts @@ -0,0 +1,58 @@ +import { ensureSodiumReady } from "@deepnotes/e2ee"; + +import type { components } from "../../api/api-types.generated"; +import { loginPreimageFromPassword, uint8ToBase64 } from "./bytes"; +import { extractRawUserKeyringsBase64FromSession } from "./session-keyrings"; + +export type UserPasswordChangeRequest = + components["schemas"]["UserPasswordChangeRequest"]; +export type UserEmailChangeConfirmRequest = + components["schemas"]["UserEmailChangeConfirmRequest"]; + +function requireSessionKeyringsMessage(): string { + return "No keyrings in this browser session. Sign in with email and password to use this action."; +} + +export async function buildPasswordChangePayload(input: { + oldPassword: string; + newPassword: string; +}): Promise { + await ensureSodiumReady(); + const raw = await extractRawUserKeyringsBase64FromSession(); + if (raw == null) { + throw new Error(requireSessionKeyringsMessage()); + } + + const oldPreimage = loginPreimageFromPassword(input.oldPassword); + const newPreimage = loginPreimageFromPassword(input.newPassword); + + return { + oldLoginHash: uint8ToBase64(oldPreimage), + newLoginHash: uint8ToBase64(newPreimage), + userEncryptedPrivateKeyring: raw.userEncryptedPrivateKeyring, + userEncryptedSymmetricKeyring: raw.userEncryptedSymmetricKeyring, + }; +} + +export async function buildEmailChangeConfirmPayload(input: { + currentPassword: string; + newPasswordAfterChange: string; + emailVerificationCode: string; +}): Promise { + await ensureSodiumReady(); + const raw = await extractRawUserKeyringsBase64FromSession(); + if (raw == null) { + throw new Error(requireSessionKeyringsMessage()); + } + + const oldPreimage = loginPreimageFromPassword(input.currentPassword); + const newPreimage = loginPreimageFromPassword(input.newPasswordAfterChange); + + return { + oldLoginHash: uint8ToBase64(oldPreimage), + emailVerificationCode: input.emailVerificationCode.trim(), + newLoginHash: uint8ToBase64(newPreimage), + userEncryptedPrivateKeyring: raw.userEncryptedPrivateKeyring, + userEncryptedSymmetricKeyring: raw.userEncryptedSymmetricKeyring, + }; +} diff --git a/new-deepnotes/apps/web/src/features/auth/build-user-register.test.ts b/new-deepnotes/apps/web/src/features/auth/build-user-register.test.ts new file mode 100644 index 00000000..d61be2bc --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/build-user-register.test.ts @@ -0,0 +1,58 @@ +import { + base64ToBytes, + createPrivateKeyring, + createSymmetricKeyring, + DataLayer, + ensureSodiumReady, +} from "@deepnotes/e2ee"; +import { describe, expect, it } from "vitest"; + +import { buildUserRegisterRequest } from "./build-user-register"; +import { loginPreimageFromPassword, uint8ToBase64 } from "./bytes"; + +describe("buildUserRegisterRequest", () => { + it("sets email, loginHash, private personal group, and raw user keyrings", async () => { + await ensureSodiumReady(); + const body = await buildUserRegisterRequest({ + email: " User@Example.com ", + password: "hunter2", + }); + expect(body.email).toBe("user@example.com"); + expect(body.loginHash).toBe( + uint8ToBase64(loginPreimageFromPassword("hunter2")), + ); + expect(body.userId).toMatch(/^[A-Za-z0-9_-]{21}$/); + expect(body.groupId).toMatch(/^[A-Za-z0-9_-]{21}$/); + expect(body.pageId).toMatch(/^[A-Za-z0-9_-]{21}$/); + expect(body.groupCreation.groupIsPublic).toBe(false); + + const priv = createPrivateKeyring( + base64ToBytes(body.userEncryptedPrivateKeyring), + ); + const sym = createSymmetricKeyring( + base64ToBytes(body.userEncryptedSymmetricKeyring), + ); + expect(priv.topLayer).toBe(DataLayer.Raw); + expect(sym.topLayer).toBe(DataLayer.Raw); + + expect(body.userEncryptedPrivateKeyring.length).toBeGreaterThan(40); + expect(body.groupCreation.groupAccessKeyring.length).toBeGreaterThan(40); + }); + + it("encrypts display name with UserName context", async () => { + await ensureSodiumReady(); + const body = await buildUserRegisterRequest({ + email: "a@b.co", + password: "x", + displayName: "Ada", + }); + const sym = createSymmetricKeyring( + base64ToBytes(body.userEncryptedSymmetricKeyring), + ); + const plain = sym.decrypt(base64ToBytes(body.userEncryptedName), { + padding: true, + associatedData: { context: "UserName", userId: body.userId }, + }); + expect(new TextDecoder().decode(plain)).toBe("Ada"); + }); +}); diff --git a/new-deepnotes/apps/web/src/features/auth/build-user-register.ts b/new-deepnotes/apps/web/src/features/auth/build-user-register.ts new file mode 100644 index 00000000..6ccc9791 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/build-user-register.ts @@ -0,0 +1,188 @@ +import { + createKeyring, + createPrivateKeyring, + createSymmetricKeyring, + ensureSodiumReady, + wrapKeyPair, + type KeyPair, + generateKeyPair, +} from "@deepnotes/e2ee"; +import { pack } from "msgpackr"; +import { nanoid } from "nanoid"; + +import type { components } from "../../api/api-types.generated"; +import { loginPreimageFromPassword, uint8ToBase64 } from "./bytes"; + +export type UserRegisterRequest = components["schemas"]["UserRegisterRequest"]; + +function textToBytes(s: string): Uint8Array { + return new TextEncoder().encode(s); +} + +function defaultNotePacked(): Uint8Array { + return pack({ + root: { noteIdxs: [0] }, + notes: [{ anchor: { y: 0 } }], + }); +} + +function defaultArrowPacked(): Uint8Array { + return pack({ color: "sky" }); +} + +/** + * Personal group matching legacy `getRegistrationValues` (private group, empty names). + */ +function buildPersonalGroupCreation(input: { + userKeyPair: KeyPair; + groupId: string; +}): UserRegisterRequest["groupCreation"] { + const accessKeyring = createSymmetricKeyring(); + const internalKeyring = createSymmetricKeyring(); + const contentKeyring = createSymmetricKeyring(); + + const rawGroupKeys = generateKeyPair(); + const groupPublicRing = createKeyring(rawGroupKeys.publicKey); + const groupPrivateRing = createPrivateKeyring(rawGroupKeys.privateKey); + + const encryptedInternalKeyring = internalKeyring.wrapAsymmetric( + input.userKeyPair, + input.userKeyPair.publicKey, + ); + + const encryptedContentKeyring = contentKeyring.wrapSymmetric( + accessKeyring.topKey, + { + associatedData: { + context: "GroupContentKeyring", + groupId: input.groupId, + }, + }, + ); + + const encryptedGroupPrivateKeyring = groupPrivateRing.wrapSymmetric( + internalKeyring.topKey, + { + associatedData: { + context: "GroupPrivateKeyring", + groupId: input.groupId, + }, + }, + ); + + const finalAccessKeyring = accessKeyring.wrapAsymmetric( + input.userKeyPair, + input.userKeyPair.publicKey, + ); + + return { + groupEncryptedName: uint8ToBase64(new Uint8Array()), + groupIsPublic: false, + groupAccessKeyring: uint8ToBase64(finalAccessKeyring.wrappedValue), + groupEncryptedInternalKeyring: uint8ToBase64( + encryptedInternalKeyring.wrappedValue, + ), + groupEncryptedContentKeyring: uint8ToBase64( + encryptedContentKeyring.wrappedValue, + ), + groupPublicKeyring: uint8ToBase64(groupPublicRing.wrappedValue), + groupEncryptedPrivateKeyring: uint8ToBase64( + encryptedGroupPrivateKeyring.wrappedValue, + ), + groupOwnerEncryptedName: uint8ToBase64(new Uint8Array()), + }; +} + +/** + * Build `POST /api/users` body: real libsodium keyrings and legacy-shaped defaults. + * Inner user keyrings are **raw** (password layer is applied only server-side); the API worker wraps them + * with `UserEncrypted*` using the password-derived key (see `performUserRegister`). + */ +export async function buildUserRegisterRequest(input: { + email: string; + password: string; + /** Plaintext display name; defaults to empty (legacy allows empty `userEncryptedName`). */ + displayName?: string; +}): Promise { + await ensureSodiumReady(); + + const email = input.email.trim().toLowerCase(); + const preimage = loginPreimageFromPassword(input.password); + const userId = nanoid(); + const groupId = nanoid(); + const pageId = nanoid(); + const displayName = input.displayName ?? ""; + + const rawUserKeys = generateKeyPair(); + const userPublicKeyring = createKeyring(rawUserKeys.publicKey); + const userPrivateKeyring = createPrivateKeyring(rawUserKeys.privateKey); + const userKeyPair = wrapKeyPair(userPublicKeyring, userPrivateKeyring); + + const userSymmetricKeyring = createSymmetricKeyring(); + + const groupCreation = buildPersonalGroupCreation({ userKeyPair, groupId }); + + const pageKeyring = createSymmetricKeyring(); + + return { + userId, + groupId, + pageId, + email, + loginHash: uint8ToBase64(preimage), + userPublicKeyring: uint8ToBase64(userPublicKeyring.wrappedValue), + userEncryptedPrivateKeyring: uint8ToBase64(userPrivateKeyring.wrappedValue), + userEncryptedSymmetricKeyring: uint8ToBase64( + userSymmetricKeyring.wrappedValue, + ), + userEncryptedName: uint8ToBase64( + userSymmetricKeyring.encrypt(textToBytes(displayName), { + padding: true, + associatedData: { + context: "UserName", + userId, + }, + }), + ), + userEncryptedDefaultNote: uint8ToBase64( + userSymmetricKeyring.encrypt(defaultNotePacked(), { + padding: true, + associatedData: { + context: "UserDefaultNote", + userId, + }, + }), + ), + userEncryptedDefaultArrow: uint8ToBase64( + userSymmetricKeyring.encrypt(defaultArrowPacked(), { + padding: true, + associatedData: { + context: "UserDefaultArrow", + userId, + }, + }), + ), + groupCreation, + pageCreation: { + pageEncryptedSymmetricKeyring: uint8ToBase64(pageKeyring.wrappedValue), + pageEncryptedRelativeTitle: uint8ToBase64( + pageKeyring.encrypt(textToBytes("Main page"), { + padding: true, + associatedData: { + context: "PageRelativeTitle", + pageId, + }, + }), + ), + pageEncryptedAbsoluteTitle: uint8ToBase64( + pageKeyring.encrypt(textToBytes(""), { + padding: true, + associatedData: { + context: "PageAbsoluteTitle", + pageId, + }, + }), + ), + }, + }; +} diff --git a/new-deepnotes/apps/web/src/features/auth/bytes.test.ts b/new-deepnotes/apps/web/src/features/auth/bytes.test.ts new file mode 100644 index 00000000..b9fc7a0d --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/bytes.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; + +import { loginPreimageFromPassword, uint8ToBase64 } from "./bytes"; + +describe("uint8ToBase64", () => { + it("encodes small buffers", () => { + const u = new Uint8Array([0, 1, 2, 255]); + expect(uint8ToBase64(u)).toBe("AAEC/w=="); + }); +}); + +describe("loginPreimageFromPassword", () => { + it("uses UTF-8 bytes", () => { + const b = loginPreimageFromPassword("hรฉllo"); + expect(b).toEqual(new TextEncoder().encode("hรฉllo")); + }); +}); diff --git a/new-deepnotes/apps/web/src/features/auth/bytes.ts b/new-deepnotes/apps/web/src/features/auth/bytes.ts new file mode 100644 index 00000000..7dd0e7a2 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/bytes.ts @@ -0,0 +1,14 @@ +/** Standard base64 (OpenAPI `format: byte`) for JSON session bodies. */ +export function uint8ToBase64(bytes: Uint8Array): string { + let binary = ""; + const len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]!); + } + return btoa(binary); +} + +/** Argon2 preimage: UTF-8 bytes of the account password (same at register and login). */ +export function loginPreimageFromPassword(password: string): Uint8Array { + return new TextEncoder().encode(password); +} diff --git a/new-deepnotes/apps/web/src/features/auth/cookies.ts b/new-deepnotes/apps/web/src/features/auth/cookies.ts new file mode 100644 index 00000000..2d4a6ca2 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/cookies.ts @@ -0,0 +1,12 @@ +/** Read nonโ€“httpOnly cookie (e.g. `loggedIn` client hint per AUTH_AND_CORS.md). */ +export function readDocumentCookie(name: string): string | undefined { + if (typeof document === "undefined") return undefined; + const parts = document.cookie.split(";").map((p) => p.trim()); + const prefix = `${name}=`; + for (const part of parts) { + if (part.startsWith(prefix)) { + return decodeURIComponent(part.slice(prefix.length)); + } + } + return undefined; +} diff --git a/new-deepnotes/apps/web/src/features/auth/crypto-storage.ts b/new-deepnotes/apps/web/src/features/auth/crypto-storage.ts new file mode 100644 index 00000000..320d92cc --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/crypto-storage.ts @@ -0,0 +1,82 @@ +/** Tab-shared crypto material for E2EE (stored in `localStorage` so auth survives new tabs). */ + +const P = "dn."; + +export const CRYPTO_KEYS = { + sessionKey: `${P}sessionKey`, + encryptedPrivateKeyring: `${P}encryptedPrivateKeyring`, + encryptedSymmetricKeyring: `${P}encryptedSymmetricKeyring`, + sessionId: `${P}sessionId`, + userId: `${P}userId`, + publicKeyring: `${P}publicKeyring`, + personalGroupId: `${P}personalGroupId`, +} as const; + +export type StoredSessionCrypto = { + sessionKeyB64: string; + encryptedPrivateKeyringB64: string; + encryptedSymmetricKeyringB64: string; + sessionId: string; + userId: string; + publicKeyringB64: string; + personalGroupId: string; +}; + +export function writeSessionCrypto(data: StoredSessionCrypto): void { + localStorage.setItem(CRYPTO_KEYS.sessionKey, data.sessionKeyB64); + localStorage.setItem( + CRYPTO_KEYS.encryptedPrivateKeyring, + data.encryptedPrivateKeyringB64, + ); + localStorage.setItem( + CRYPTO_KEYS.encryptedSymmetricKeyring, + data.encryptedSymmetricKeyringB64, + ); + localStorage.setItem(CRYPTO_KEYS.sessionId, data.sessionId); + localStorage.setItem(CRYPTO_KEYS.userId, data.userId); + localStorage.setItem(CRYPTO_KEYS.publicKeyring, data.publicKeyringB64); + localStorage.setItem( + CRYPTO_KEYS.personalGroupId, + data.personalGroupId, + ); +} + +export function readSessionCrypto(): StoredSessionCrypto | null { + const sessionKeyB64 = localStorage.getItem(CRYPTO_KEYS.sessionKey); + const encryptedPrivateKeyringB64 = localStorage.getItem( + CRYPTO_KEYS.encryptedPrivateKeyring, + ); + const encryptedSymmetricKeyringB64 = localStorage.getItem( + CRYPTO_KEYS.encryptedSymmetricKeyring, + ); + const sessionId = localStorage.getItem(CRYPTO_KEYS.sessionId); + const userId = localStorage.getItem(CRYPTO_KEYS.userId); + const publicKeyringB64 = localStorage.getItem(CRYPTO_KEYS.publicKeyring); + const personalGroupId = localStorage.getItem(CRYPTO_KEYS.personalGroupId); + if ( + sessionKeyB64 == null || + encryptedPrivateKeyringB64 == null || + encryptedSymmetricKeyringB64 == null || + sessionId == null || + userId == null || + publicKeyringB64 == null || + personalGroupId == null + ) { + return null; + } + return { + sessionKeyB64, + encryptedPrivateKeyringB64, + encryptedSymmetricKeyringB64, + sessionId, + userId, + publicKeyringB64, + personalGroupId, + }; +} + +export function clearSessionCrypto(): void { + for (const k of Object.values(CRYPTO_KEYS)) { + localStorage.removeItem(k); + } +} diff --git a/new-deepnotes/apps/web/src/features/auth/session-keyrings.test.ts b/new-deepnotes/apps/web/src/features/auth/session-keyrings.test.ts new file mode 100644 index 00000000..de161b5c --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/session-keyrings.test.ts @@ -0,0 +1,187 @@ +import { randomBytes } from "node:crypto"; + +import { + base64ToBytes, + createPrivateKeyring, + createSymmetricKeyring, + DataLayer, + derivePasswordValues, + ensureSodiumReady, +} from "@deepnotes/e2ee"; +import { beforeEach, describe, expect, it } from "vitest"; +import { nanoid } from "nanoid"; + +import { buildUserRegisterRequest } from "./build-user-register"; +import { loginPreimageFromPassword, uint8ToBase64 } from "./bytes"; +import { buildPasswordChangePayload } from "./build-password-and-email-confirm"; +import { clearSessionCrypto, readSessionCrypto } from "./crypto-storage"; +import { + applyRefreshToStoredKeyrings, + extractRawUserKeyringsBase64FromSession, + persistSessionKeyringsFromLogin, +} from "./session-keyrings"; + +describe("session keyrings", () => { + beforeEach(() => { + clearSessionCrypto(); + }); + + it("persistSessionKeyringsFromLogin is a no-op when passwordSalt is missing", async () => { + await ensureSodiumReady(); + await persistSessionKeyringsFromLogin({ + login: { + userId: "u1", + sessionId: "s1", + sessionKey: uint8ToBase64(randomBytes(32)), + personalGroupId: "g1", + publicKeyring: uint8ToBase64(randomBytes(32)), + encryptedPrivateKeyring: uint8ToBase64(randomBytes(32)), + encryptedSymmetricKeyring: uint8ToBase64(randomBytes(32)), + }, + password: "x", + }); + expect(readSessionCrypto()).toBe(null); + }); + + it("raw login keyrings persist, round-trip through session, and buildPasswordChangePayload", async () => { + await ensureSodiumReady(); + const password = "correct-horse-battery"; + const reg = await buildUserRegisterRequest({ + email: `${nanoid()}@example.com`, + password, + }); + const sessionKey = uint8ToBase64(randomBytes(32)); + const saltB64 = uint8ToBase64(randomBytes(16)); + + await persistSessionKeyringsFromLogin({ + login: { + userId: reg.userId, + sessionId: nanoid(), + sessionKey, + personalGroupId: reg.groupId, + publicKeyring: reg.userPublicKeyring, + encryptedPrivateKeyring: reg.userEncryptedPrivateKeyring, + encryptedSymmetricKeyring: reg.userEncryptedSymmetricKeyring, + passwordSalt: saltB64, + }, + password, + }); + + const stored = readSessionCrypto(); + expect(stored?.sessionKeyB64).toBe(sessionKey); + expect(stored?.userId).toBe(reg.userId); + + const raw = await extractRawUserKeyringsBase64FromSession(); + expect(raw).not.toBe(null); + expect(raw!.userEncryptedPrivateKeyring).toBe(reg.userEncryptedPrivateKeyring); + expect(raw!.userEncryptedSymmetricKeyring).toBe( + reg.userEncryptedSymmetricKeyring, + ); + + const change = await buildPasswordChangePayload({ + oldPassword: password, + newPassword: "new-strong-pass", + }); + expect(change.userEncryptedPrivateKeyring).toBe( + reg.userEncryptedPrivateKeyring, + ); + expect(change.userEncryptedSymmetricKeyring).toBe( + reg.userEncryptedSymmetricKeyring, + ); + expect(change.oldLoginHash).toBe( + uint8ToBase64(loginPreimageFromPassword(password)), + ); + }); + + it("applyRefreshToStoredKeyrings preserves raw material after extract", async () => { + await ensureSodiumReady(); + const password = "another-pass-phrase"; + const reg = await buildUserRegisterRequest({ + email: `${nanoid()}@example.com`, + password, + }); + + await persistSessionKeyringsFromLogin({ + login: { + userId: reg.userId, + sessionId: nanoid(), + sessionKey: uint8ToBase64(randomBytes(32)), + personalGroupId: reg.groupId, + publicKeyring: reg.userPublicKeyring, + encryptedPrivateKeyring: reg.userEncryptedPrivateKeyring, + encryptedSymmetricKeyring: reg.userEncryptedSymmetricKeyring, + passwordSalt: uint8ToBase64(randomBytes(16)), + }, + password, + }); + + const before = await extractRawUserKeyringsBase64FromSession(); + expect(before).not.toBe(null); + + const oldSk = readSessionCrypto()!.sessionKeyB64; + const newSk = uint8ToBase64(randomBytes(32)); + await applyRefreshToStoredKeyrings({ + oldSessionKey: oldSk, + newSessionKey: newSk, + }); + + expect(readSessionCrypto()!.sessionKeyB64).toBe(newSk); + const after = await extractRawUserKeyringsBase64FromSession(); + expect(after).toEqual(before); + }); + + it("unwraps legacy UserPrivate / UserSymmetric outer layers then re-wraps session", async () => { + await ensureSodiumReady(); + const password = "legacy-style-wrapped"; + const reg = await buildUserRegisterRequest({ + email: `${nanoid()}@example.com`, + password, + }); + const salt = randomBytes(16); + const saltB64 = uint8ToBase64(salt); + const masterKey = derivePasswordValues({ + password: loginPreimageFromPassword(password), + salt, + }).key; + + const privLayered = createPrivateKeyring( + base64ToBytes(reg.userEncryptedPrivateKeyring), + ).wrapSymmetric(masterKey, { + associatedData: { + context: "UserPrivateKeyring", + userId: reg.userId, + }, + }); + const symLayered = createSymmetricKeyring( + base64ToBytes(reg.userEncryptedSymmetricKeyring), + ).wrapSymmetric(masterKey, { + associatedData: { + context: "UserSymmetricKeyring", + userId: reg.userId, + }, + }); + + expect(privLayered.topLayer).not.toBe(DataLayer.Raw); + expect(symLayered.topLayer).not.toBe(DataLayer.Raw); + + await persistSessionKeyringsFromLogin({ + login: { + userId: reg.userId, + sessionId: nanoid(), + sessionKey: uint8ToBase64(randomBytes(32)), + personalGroupId: reg.groupId, + publicKeyring: reg.userPublicKeyring, + encryptedPrivateKeyring: uint8ToBase64(privLayered.wrappedValue), + encryptedSymmetricKeyring: uint8ToBase64(symLayered.wrappedValue), + passwordSalt: saltB64, + }, + password, + }); + + const raw = await extractRawUserKeyringsBase64FromSession(); + expect(raw!.userEncryptedPrivateKeyring).toBe(reg.userEncryptedPrivateKeyring); + expect(raw!.userEncryptedSymmetricKeyring).toBe( + reg.userEncryptedSymmetricKeyring, + ); + }); +}); diff --git a/new-deepnotes/apps/web/src/features/auth/session-keyrings.ts b/new-deepnotes/apps/web/src/features/auth/session-keyrings.ts new file mode 100644 index 00000000..2601a209 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/session-keyrings.ts @@ -0,0 +1,201 @@ +import { + base64ToBytes, + createPrivateKeyring, + createSymmetricKeyring, + DataLayer, + derivePasswordValues, + ensureSodiumReady, + wrapSymmetricKey, +} from "@deepnotes/e2ee"; + +import { loginPreimageFromPassword, uint8ToBase64 } from "./bytes"; +import { + clearSessionCrypto, + readSessionCrypto, + writeSessionCrypto, + type StoredSessionCrypto, +} from "./crypto-storage"; + +export type LoginSuccessPayload = { + userId: string; + sessionId: string; + sessionKey: string; + personalGroupId: string; + publicKeyring: string; + encryptedPrivateKeyring: string; + encryptedSymmetricKeyring: string; + passwordSalt?: string; +}; + +/** + * After password login: unwrap `User*Keyring` with the password-derived key, + * re-wrap with `SessionUser*Keyring`, and persist (plus raw session key for unwrap). + */ +export async function persistSessionKeyringsFromLogin(input: { + login: LoginSuccessPayload; + password: string; +}): Promise { + const { login, password } = input; + if (login.passwordSalt == null || login.passwordSalt === "") { + return; + } + await ensureSodiumReady(); + const preimage = loginPreimageFromPassword(password); + const masterKey = derivePasswordValues({ + password: preimage, + salt: base64ToBytes(login.passwordSalt), + }).key; + const sessionKey = wrapSymmetricKey(base64ToBytes(login.sessionKey)); + + let privateKeyring = createPrivateKeyring( + base64ToBytes(login.encryptedPrivateKeyring), + ); + let symmetricKeyring = createSymmetricKeyring( + base64ToBytes(login.encryptedSymmetricKeyring), + ); + + // Legacy accounts: extra `UserPrivateKeyring` / `UserSymmetricKeyring` layer under + // `UserEncrypted*`. Greenfield register sends raw inner keyrings; server unwraps once. + if (privateKeyring.topLayer !== DataLayer.Raw) { + privateKeyring = privateKeyring.unwrapSymmetric(masterKey, { + associatedData: { + context: "UserPrivateKeyring", + userId: login.userId, + }, + }); + } + if (symmetricKeyring.topLayer !== DataLayer.Raw) { + symmetricKeyring = symmetricKeyring.unwrapSymmetric(masterKey, { + associatedData: { + context: "UserSymmetricKeyring", + userId: login.userId, + }, + }); + } + + const encPrivB64 = uint8ToBase64( + privateKeyring.wrapSymmetric(sessionKey, { + associatedData: { + context: "SessionUserPrivateKeyring", + userId: login.userId, + }, + }).wrappedValue, + ); + + const encSymB64 = uint8ToBase64( + symmetricKeyring.wrapSymmetric(sessionKey, { + associatedData: { + context: "SessionUserSymmetricKeyring", + userId: login.userId, + }, + }).wrappedValue, + ); + + writeSessionCrypto({ + sessionKeyB64: login.sessionKey, + encryptedPrivateKeyringB64: encPrivB64, + encryptedSymmetricKeyringB64: encSymB64, + sessionId: login.sessionId, + userId: login.userId, + publicKeyringB64: login.publicKeyring, + personalGroupId: login.personalGroupId, + }); +} + +/** Rotate session-wrapped keyrings after `POST /api/sessions/refresh` (cookie rotation). */ +export async function applyRefreshToStoredKeyrings(input: { + oldSessionKey: string; + newSessionKey: string; +}): Promise { + const stored = readSessionCrypto(); + if (stored == null) { + return; + } + await ensureSodiumReady(); + const oldSk = wrapSymmetricKey(base64ToBytes(input.oldSessionKey)); + const newSk = wrapSymmetricKey(base64ToBytes(input.newSessionKey)); + const { userId } = stored; + + const privateKeyring = createPrivateKeyring( + base64ToBytes(stored.encryptedPrivateKeyringB64), + ).unwrapSymmetric(oldSk, { + associatedData: { + context: "SessionUserPrivateKeyring", + userId, + }, + }); + + const symmetricKeyring = createSymmetricKeyring( + base64ToBytes(stored.encryptedSymmetricKeyringB64), + ).unwrapSymmetric(oldSk, { + associatedData: { + context: "SessionUserSymmetricKeyring", + userId, + }, + }); + + const next: StoredSessionCrypto = { + ...stored, + sessionKeyB64: input.newSessionKey, + encryptedPrivateKeyringB64: uint8ToBase64( + privateKeyring.wrapSymmetric(newSk, { + associatedData: { + context: "SessionUserPrivateKeyring", + userId, + }, + }).wrappedValue, + ), + encryptedSymmetricKeyringB64: uint8ToBase64( + symmetricKeyring.wrapSymmetric(newSk, { + associatedData: { + context: "SessionUserSymmetricKeyring", + userId, + }, + }).wrappedValue, + ), + }; + writeSessionCrypto(next); +} + +/** + * Unwrap stored session keyrings to the **raw** inner blobs the API expects when + * re-wraping after password change (`UserEncrypted*` / register-shaped bodies). + */ +export async function extractRawUserKeyringsBase64FromSession(): Promise<{ + userEncryptedPrivateKeyring: string; + userEncryptedSymmetricKeyring: string; +} | null> { + const stored = readSessionCrypto(); + if (stored == null) { + return null; + } + await ensureSodiumReady(); + const sessionKey = wrapSymmetricKey(base64ToBytes(stored.sessionKeyB64)); + const { userId } = stored; + + const privateKeyring = createPrivateKeyring( + base64ToBytes(stored.encryptedPrivateKeyringB64), + ).unwrapSymmetric(sessionKey, { + associatedData: { + context: "SessionUserPrivateKeyring", + userId, + }, + }); + + const symmetricKeyring = createSymmetricKeyring( + base64ToBytes(stored.encryptedSymmetricKeyringB64), + ).unwrapSymmetric(sessionKey, { + associatedData: { + context: "SessionUserSymmetricKeyring", + userId, + }, + }); + + return { + userEncryptedPrivateKeyring: uint8ToBase64(privateKeyring.wrappedValue), + userEncryptedSymmetricKeyring: uint8ToBase64(symmetricKeyring.wrappedValue), + }; +} + +export { clearSessionCrypto, readSessionCrypto }; +export type { StoredSessionCrypto }; diff --git a/new-deepnotes/apps/web/src/features/auth/useSession.test.ts b/new-deepnotes/apps/web/src/features/auth/useSession.test.ts new file mode 100644 index 00000000..3b7459a8 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/useSession.test.ts @@ -0,0 +1,171 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const stubB64 = "dGVzdA=="; +const stubB64b = "dGVzdGI="; + +const userMe = { + userId: "u_test", + emailVerified: true, + personalGroupId: "g_test", +}; + +const loginSuccessNoSalt = { + userId: "u_test", + sessionId: "ses_test", + sessionKey: stubB64, + personalGroupId: "g_test", + publicKeyring: stubB64, + encryptedPrivateKeyring: stubB64, + encryptedSymmetricKeyring: stubB64, +}; + +const { mockGet, mockPost } = vi.hoisted(() => ({ + mockGet: vi.fn(), + mockPost: vi.fn(), +})); + +vi.mock("../../api/client", () => ({ + createDeepnotesApiClient: vi.fn(() => ({ + GET: mockGet, + POST: mockPost, + })), +})); + +import { + resetSessionSingletonForTests, + TWO_FACTOR_MESSAGE, + useSession, +} from "./useSession"; + +function okFetch(status: number, data: T) { + return Promise.resolve({ + data, + error: undefined, + response: { status } as Pick as Response, + }); +} + +describe("useSession", () => { + beforeEach(() => { + resetSessionSingletonForTests(); + mockGet.mockReset(); + mockPost.mockReset(); + document.cookie = ""; + }); + + it("loginWithPassword succeeds and loads /me (no passwordSalt skips keyring unwrap)", async () => { + mockPost.mockImplementation((path: string) => { + if (path === "/api/sessions/login") { + return okFetch(200, loginSuccessNoSalt); + } + return okFetch(404, undefined as never); + }); + mockGet.mockImplementation((path: string) => { + if (path === "/api/users/me") return okFetch(200, userMe); + return okFetch(404, undefined as never); + }); + + const { loginWithPassword, user, lastError } = useSession(); + const result = await loginWithPassword({ + email: "user@Example.com ", + password: "secret", + rememberSession: true, + }); + + expect(result).toEqual({ ok: true, needTwoFactor: false }); + expect(lastError.value).toBe(null); + expect(user.value?.userId).toBe("u_test"); + expect(mockPost.mock.calls.some((c) => c[0] === "/api/sessions/login")).toBe(true); + const loginBody = mockPost.mock.calls.find((c) => c[0] === "/api/sessions/login")?.[1] as { + body: { email: string }; + }; + expect(loginBody.body.email).toBe("user@example.com"); + }); + + it("loginWithPassword returns needTwoFactor on 401 with server 2FA message", async () => { + mockPost.mockImplementation((path: string) => { + if (path === "/api/sessions/login") { + return Promise.resolve({ + data: undefined, + error: { + code: "UNAUTHORIZED", + message: TWO_FACTOR_MESSAGE, + }, + response: { status: 401 } as Pick as Response, + }); + } + return okFetch(404, undefined as never); + }); + + const { loginWithPassword, lastError, twoFactorRequired } = useSession(); + const result = await loginWithPassword({ + email: "a@b.co", + password: "x", + rememberSession: false, + }); + + expect(result).toEqual({ ok: false, needTwoFactor: true }); + expect(twoFactorRequired.value).toBe(true); + expect(lastError.value).toBe(TWO_FACTOR_MESSAGE); + }); + + it("logout clears user on 204", async () => { + mockPost.mockImplementation((path: string) => { + if (path === "/api/sessions/login") { + return okFetch(200, loginSuccessNoSalt); + } + if (path === "/api/sessions/logout") { + return Promise.resolve({ + data: undefined, + error: undefined, + response: { status: 204 } as Pick as Response, + }); + } + return okFetch(404, undefined as never); + }); + mockGet.mockImplementation((path: string) => { + if (path === "/api/users/me") return okFetch(200, userMe); + return okFetch(404, undefined as never); + }); + + const { loginWithPassword, logout, user } = useSession(); + await loginWithPassword({ + email: "z@z.co", + password: "pw", + rememberSession: false, + }); + expect(user.value).not.toBe(null); + + await logout(); + expect(user.value).toBe(null); + }); + + it("bootstrap does not call refresh when loggedIn cookie is absent", async () => { + const { bootstrap, bootstrapped, user } = useSession(); + await bootstrap(); + expect(mockPost.mock.calls.map((c) => c[0])).not.toContain("/api/sessions/refresh"); + expect(user.value).toBe(null); + expect(bootstrapped.value).toBe(true); + }); + + it("bootstrap refreshes then loads /me when loggedIn cookie is set", async () => { + document.cookie = "loggedIn=true"; + + mockPost.mockImplementation((path: string) => { + if (path === "/api/sessions/refresh") { + return okFetch(200, { oldSessionKey: stubB64, newSessionKey: stubB64b }); + } + return okFetch(404, undefined as never); + }); + mockGet.mockImplementation((path: string) => { + if (path === "/api/users/me") return okFetch(200, userMe); + return okFetch(404, undefined as never); + }); + + const { bootstrap, user } = useSession(); + await bootstrap(); + + expect(mockPost.mock.calls.map((c) => c[0])).toContain("/api/sessions/refresh"); + expect(user.value?.userId).toBe("u_test"); + }); +}); diff --git a/new-deepnotes/apps/web/src/features/auth/useSession.ts b/new-deepnotes/apps/web/src/features/auth/useSession.ts new file mode 100644 index 00000000..a7486a45 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/useSession.ts @@ -0,0 +1,225 @@ +import { computed, ref, type Ref } from "vue"; + +import { createDeepnotesApiClient } from "../../api/client"; +import type { components } from "../../api/api-types.generated"; +import { loginPreimageFromPassword, uint8ToBase64 } from "./bytes"; +import { readDocumentCookie } from "./cookies"; +import { + applyRefreshToStoredKeyrings, + clearSessionCrypto, + persistSessionKeyringsFromLogin, +} from "./session-keyrings"; + +export type UserMe = components["schemas"]["UserMeResponse"]; +export type SessionErrorBody = components["schemas"]["SessionErrorResponse"]; + +const TWO_FACTOR_MESSAGE = "Requires two-factor authentication."; + +let client = createDeepnotesApiClient(); + +const user: Ref = ref(null); +const loading: Ref = ref(false); +const bootstrapped: Ref = ref(false); +const lastError: Ref = ref(null); +const twoFactorRequired: Ref = ref(false); + +let bootstrapInFlight: Promise | null = null; + +function setErrorFromBody(body: unknown, fallback: string) { + if ( + body && + typeof body === "object" && + "message" in body && + typeof (body as SessionErrorBody).message === "string" + ) { + lastError.value = (body as SessionErrorBody).message; + return; + } + lastError.value = fallback; +} + +/** + * Resets {@link useSession} module state between Vitest cases (singleton refs). + */ +export function resetSessionSingletonForTests(): void { + user.value = null; + loading.value = false; + bootstrapped.value = false; + lastError.value = null; + twoFactorRequired.value = false; + bootstrapInFlight = null; + client = createDeepnotesApiClient(); +} + +export function useSession() { + const isAuthenticated = computed(() => user.value != null); + const loggedInHint = computed( + () => readDocumentCookie("loggedIn") === "true", + ); + + async function fetchMe() { + const { data, error, response } = await client.GET("/api/users/me", {}); + if (response.status === 200 && data) { + user.value = data; + return true; + } + if (error != null) { + setErrorFromBody(error, "Could not load account."); + } + user.value = null; + return false; + } + + /** + * If the `loggedIn` hint cookie is set, rotate refresh token then load `/me`. + * Safe to call from multiple components; concurrent callers share one run. + */ + async function bootstrap() { + if (bootstrapped.value) { + return; + } + if (bootstrapInFlight) { + await bootstrapInFlight; + return; + } + loading.value = true; + lastError.value = null; + bootstrapInFlight = (async () => { + try { + if (readDocumentCookie("loggedIn") !== "true") { + user.value = null; + return; + } + const refRes = await client.POST("/api/sessions/refresh", {}); + if (refRes.response.status === 401) { + user.value = null; + clearSessionCrypto(); + return; + } + if (refRes.error != null) { + user.value = null; + clearSessionCrypto(); + setErrorFromBody( + refRes.error, + "Session could not be refreshed. Try signing in again.", + ); + return; + } + if (refRes.response.status === 200 && refRes.data) { + try { + await applyRefreshToStoredKeyrings({ + oldSessionKey: refRes.data.oldSessionKey, + newSessionKey: refRes.data.newSessionKey, + }); + } catch { + user.value = null; + clearSessionCrypto(); + return; + } + } + await fetchMe(); + } finally { + loading.value = false; + bootstrapped.value = true; + bootstrapInFlight = null; + } + })(); + await bootstrapInFlight; + } + + async function loginWithPassword(input: { + email: string; + password: string; + rememberSession: boolean; + authenticatorToken?: string; + recoveryCode?: string; + }): Promise<{ ok: boolean; needTwoFactor: boolean }> { + loading.value = true; + if (!input.authenticatorToken && !input.recoveryCode) { + twoFactorRequired.value = false; + } + lastError.value = null; + const preimage = loginPreimageFromPassword(input.password); + const body: components["schemas"]["SessionLoginRequest"] = { + email: input.email.trim().toLowerCase(), + loginHash: uint8ToBase64(preimage), + rememberSession: input.rememberSession, + ...(input.authenticatorToken + ? { authenticatorToken: input.authenticatorToken } + : {}), + ...(input.recoveryCode ? { recoveryCode: input.recoveryCode } : {}), + }; + try { + const { data, error, response } = await client.POST( + "/api/sessions/login", + { body }, + ); + if (response.status === 200 && data) { + twoFactorRequired.value = false; + user.value = null; + if (data.passwordSalt != null && data.passwordSalt !== "") { + await persistSessionKeyringsFromLogin({ + login: data, + password: input.password, + }); + } + await fetchMe(); + return { ok: true, needTwoFactor: false }; + } + if (response.status === 401 && error) { + setErrorFromBody(error, "Sign-in failed."); + const errBody = error as SessionErrorBody; + if (errBody.message === TWO_FACTOR_MESSAGE) { + twoFactorRequired.value = true; + return { ok: false, needTwoFactor: true }; + } + return { ok: false, needTwoFactor: false }; + } + if (error != null) { + setErrorFromBody(error, "Sign-in failed."); + } else { + lastError.value = "Sign-in failed."; + } + return { ok: false, needTwoFactor: false }; + } finally { + loading.value = false; + } + } + + async function logout() { + loading.value = true; + lastError.value = null; + try { + const { response } = await client.POST("/api/sessions/logout", {}); + if (response.status === 204) { + user.value = null; + twoFactorRequired.value = false; + clearSessionCrypto(); + } else { + lastError.value = "Sign out failed."; + } + } finally { + loading.value = false; + } + } + + return { + client, + user, + loading, + bootstrapped, + lastError, + twoFactorRequired, + isAuthenticated, + loggedInHint, + bootstrap, + fetchMe, + loginWithPassword, + logout, + clearError: () => { + lastError.value = null; + }, + }; +} + +export { TWO_FACTOR_MESSAGE }; diff --git a/new-deepnotes/apps/web/src/features/groups/GroupDetailView.vue b/new-deepnotes/apps/web/src/features/groups/GroupDetailView.vue new file mode 100644 index 00000000..6195fc99 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/GroupDetailView.vue @@ -0,0 +1,757 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/groups/GroupInviteLandingView.vue b/new-deepnotes/apps/web/src/features/groups/GroupInviteLandingView.vue new file mode 100644 index 00000000..1f3c9d7f --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/GroupInviteLandingView.vue @@ -0,0 +1,196 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/groups/GroupJoinRequestView.vue b/new-deepnotes/apps/web/src/features/groups/GroupJoinRequestView.vue new file mode 100644 index 00000000..a02279be --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/GroupJoinRequestView.vue @@ -0,0 +1,164 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/groups/GroupsView.vue b/new-deepnotes/apps/web/src/features/groups/GroupsView.vue new file mode 100644 index 00000000..1df98c84 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/GroupsView.vue @@ -0,0 +1,148 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/groups/group-make-private-crypto.ts b/new-deepnotes/apps/web/src/features/groups/group-make-private-crypto.ts new file mode 100644 index 00000000..86d61e40 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/group-make-private-crypto.ts @@ -0,0 +1,273 @@ +import { + base64ToBytes, + createKeyring, + createPrivateKeyring, + createSymmetricKeyring, + DataLayer, + ensureSodiumReady, + wrapKeyPair, + wrapSymmetricKey, + generateKeyPair, +} from "@deepnotes/e2ee"; + +import { uint8ToBase64 } from "../auth/bytes"; +import type { StoredSessionCrypto } from "../auth/crypto-storage"; + +import type { components } from "@/api/api-types.generated"; + +export type GroupPrivacyMakePrivateBootstrapJson = { + groupAccessKeyring: string | null; + groupEncryptedName: string; + groupEncryptedContentKeyring: string; + groupPublicKeyring: string; + groupEncryptedPrivateKeyring: string; + groupEncryptedAccessKeyring: string | null; + groupEncryptedInternalKeyring: string; + groupMembers: Record< + string, + { publicKeyring: string; encryptedName: string | null } + >; + groupJoinInvitations: Record< + string, + { publicKeyring: string; encryptedName: string } + >; + groupJoinRequests: Record; + groupPages: Record; +}; + +type Out = components["schemas"]["GroupPrivacyPrivateRequest"]; + +/** + * Build `POST โ€ฆ/privacy/private` JSON (legacy WS `groups.privacy.makePrivate` step 2). + * `groupIsPublic` must be **false** so the shared `access_keyring` is cleared and per-member access blobs are written. + */ +export async function buildGroupPrivacyMakePrivateRequest(input: { + groupId: string; + /** Always `false` for make-private (clears group-level access keyring). */ + groupIsPublic: boolean; + stored: StoredSessionCrypto; + bootstrap: GroupPrivacyMakePrivateBootstrapJson; +}): Promise { + if (input.groupIsPublic) { + throw new Error("groupIsPublic must be false for make-private."); + } + + await ensureSodiumReady(); + const sessionKey = wrapSymmetricKey( + base64ToBytes(input.stored.sessionKeyB64), + ); + const { userId } = input.stored; + + const publicKeyring = createKeyring( + base64ToBytes(input.stored.publicKeyringB64), + ); + const privateKeyring = createPrivateKeyring( + base64ToBytes(input.stored.encryptedPrivateKeyringB64), + ).unwrapSymmetric(sessionKey, { + associatedData: { + context: "SessionUserPrivateKeyring", + userId, + }, + }); + const keyPair = wrapKeyPair(publicKeyring, privateKeyring); + + const b = input.bootstrap; + const accessFromGroup = + b.groupAccessKeyring != null && b.groupAccessKeyring.length > 0 + ? base64ToBytes(b.groupAccessKeyring) + : null; + const accessFromMember = + b.groupEncryptedAccessKeyring != null && + b.groupEncryptedAccessKeyring.length > 0 + ? base64ToBytes(b.groupEncryptedAccessKeyring) + : null; + + const oldGroupAccessKeyring = + accessFromGroup != null + ? createSymmetricKeyring(accessFromGroup) + : createSymmetricKeyring(accessFromMember!).unwrapAsymmetric( + keyPair.privateKey, + ); + + const oldGroupInternalKeyring = createSymmetricKeyring( + base64ToBytes(b.groupEncryptedInternalKeyring), + ).unwrapAsymmetric(keyPair.privateKey); + + const oldGroupPublicKeyring = createKeyring(base64ToBytes(b.groupPublicKeyring)); + const oldGroupPrivateKeyring = createPrivateKeyring( + base64ToBytes(b.groupEncryptedPrivateKeyring), + ).unwrapSymmetric(oldGroupInternalKeyring, { + associatedData: { + context: "GroupPrivateKeyring", + groupId: input.groupId, + }, + }); + + const oldGroupContentKeyring = createSymmetricKeyring( + base64ToBytes(b.groupEncryptedContentKeyring), + ).unwrapSymmetric(oldGroupAccessKeyring, { + associatedData: { + context: "GroupContentKeyring", + groupId: input.groupId, + }, + }); + + if (oldGroupContentKeyring.hasLayer(DataLayer.Symmetric)) { + throw new Error( + "This group uses a password-protected content keyring. Make-private from the browser is not supported until group password unlock is implemented.", + ); + } + + const newGroupAccessKeyring = oldGroupAccessKeyring.addKey(); + const newGroupInternalKeyring = oldGroupInternalKeyring.addKey(); + const newGroupContentKeyring = oldGroupContentKeyring.addKey(); + + const newGroupRawKeypair = generateKeyPair(); + const newGroupPublicKeyring = oldGroupPublicKeyring.addKey( + newGroupRawKeypair.publicKey, + ); + const newGroupPrivateKeyring = oldGroupPrivateKeyring.addKey( + newGroupRawKeypair.privateKey, + ); + + const groupEncNameBytes = base64ToBytes(b.groupEncryptedName); + const groupEncryptedNameOut = + groupEncNameBytes.byteLength === 0 + ? new Uint8Array(0) + : newGroupAccessKeyring.encrypt( + oldGroupAccessKeyring.decrypt(groupEncNameBytes, { + padding: true, + associatedData: { + context: "GroupName", + groupId: input.groupId, + }, + }), + { + padding: true, + associatedData: { + context: "GroupName", + groupId: input.groupId, + }, + }, + ); + + const groupEncryptedContentKeyringOut = newGroupContentKeyring + .wrapSymmetric(newGroupAccessKeyring, { + associatedData: { + context: "GroupContentKeyring", + groupId: input.groupId, + }, + }) + .wrappedValue; + + const groupEncryptedPrivateKeyringOut = newGroupPrivateKeyring + .wrapSymmetric(newGroupInternalKeyring, { + associatedData: { + context: "GroupPrivateKeyring", + groupId: input.groupId, + }, + }) + .wrappedValue; + + const groupMembers: Out["groupMembers"] = {}; + for (const [uid, m] of Object.entries(b.groupMembers)) { + const recvPk = createKeyring(base64ToBytes(m.publicKeyring)); + const encAccess = newGroupAccessKeyring.wrapAsymmetric( + keyPair, + recvPk, + ).wrappedValue; + const encInternal = newGroupInternalKeyring.wrapAsymmetric( + keyPair, + recvPk, + ).wrappedValue; + let encName: string | null = null; + if (m.encryptedName != null && m.encryptedName.length > 0) { + encName = uint8ToBase64( + newGroupPrivateKeyring.encrypt( + oldGroupPrivateKeyring.decrypt(base64ToBytes(m.encryptedName), { + padding: true, + }), + newGroupPublicKeyring, + newGroupPublicKeyring, + { padding: true }, + ), + ); + } + groupMembers[uid] = { + encryptedAccessKeyring: uint8ToBase64(encAccess), + encryptedInternalKeyring: uint8ToBase64(encInternal), + encryptedName: encName, + }; + } + + const groupJoinInvitations: Out["groupJoinInvitations"] = {}; + for (const [uid, inv] of Object.entries(b.groupJoinInvitations)) { + const recvPk = createKeyring(base64ToBytes(inv.publicKeyring)); + groupJoinInvitations[uid] = { + encryptedAccessKeyring: uint8ToBase64( + newGroupAccessKeyring.wrapAsymmetric(keyPair, recvPk).wrappedValue, + ), + encryptedInternalKeyring: uint8ToBase64( + newGroupInternalKeyring.wrapAsymmetric(keyPair, recvPk).wrappedValue, + ), + encryptedName: uint8ToBase64( + newGroupPrivateKeyring.encrypt( + oldGroupPrivateKeyring.decrypt(base64ToBytes(inv.encryptedName), { + padding: true, + }), + newGroupPublicKeyring, + newGroupPublicKeyring, + { padding: true }, + ), + ), + }; + } + + const groupJoinRequests: Out["groupJoinRequests"] = {}; + for (const [uid, jr] of Object.entries(b.groupJoinRequests)) { + groupJoinRequests[uid] = { + encryptedName: uint8ToBase64( + newGroupPrivateKeyring.encrypt( + oldGroupPrivateKeyring.decrypt(base64ToBytes(jr.encryptedName), { + padding: true, + }), + newGroupPublicKeyring, + newGroupPublicKeyring, + { padding: true }, + ), + ), + }; + } + + const groupPages: Out["groupPages"] = {}; + for (const [pageId, pg] of Object.entries(b.groupPages)) { + groupPages[pageId] = { + encryptedSymmetricKeyring: uint8ToBase64( + createSymmetricKeyring(base64ToBytes(pg.encryptedSymmetricKeyring)) + .unwrapSymmetric(oldGroupContentKeyring, { + associatedData: { + context: "PageKeyring", + pageId, + }, + }) + .wrapSymmetric(newGroupContentKeyring, { + associatedData: { + context: "PageKeyring", + pageId, + }, + }).wrappedValue, + ), + }; + } + + return { + groupEncryptedName: uint8ToBase64(groupEncryptedNameOut), + groupEncryptedContentKeyring: uint8ToBase64(groupEncryptedContentKeyringOut), + groupPublicKeyring: uint8ToBase64(newGroupPublicKeyring.wrappedValue), + groupEncryptedPrivateKeyring: uint8ToBase64(groupEncryptedPrivateKeyringOut), + groupMembers, + groupJoinInvitations, + groupJoinRequests, + groupPages, + }; +} diff --git a/new-deepnotes/apps/web/src/features/groups/group-members-detail.test.ts b/new-deepnotes/apps/web/src/features/groups/group-members-detail.test.ts new file mode 100644 index 00000000..9d6af8a2 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/group-members-detail.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it, vi } from "vitest"; + +import { fetchGroupMembersDetail } from "./group-members-detail"; + +describe("fetchGroupMembersDetail", () => { + it("returns data on 200", async () => { + const payload = { + viewerUserId: "u1", + viewerRole: "owner" as const, + groupIsPublic: true, + joinRequestsAllowed: true, + members: [{ userId: "u1", role: "owner" as const }], + pendingInvitations: [], + pendingJoinRequests: [], + }; + const client = { + GET: vi.fn().mockResolvedValue({ + response: { status: 200 }, + data: payload, + error: undefined, + }), + }; + const out = await fetchGroupMembersDetail({ + client: client as never, + groupId: "aaaaaaaaaaaaaaaaaaaaa", + }); + expect(out.data).toEqual(payload); + expect(out.error).toBeNull(); + expect(client.GET).toHaveBeenCalledWith( + "/api/groups/{groupId}/members/detail", + { params: { path: { groupId: "aaaaaaaaaaaaaaaaaaaaa" } } }, + ); + }); + + it("returns error on failure", async () => { + const client = { + GET: vi.fn().mockResolvedValue({ + response: { status: 403 }, + data: undefined, + error: { message: "Insufficient permissions." }, + }), + }; + const out = await fetchGroupMembersDetail({ + client: client as never, + groupId: "aaaaaaaaaaaaaaaaaaaaa", + }); + expect(out.data).toBeNull(); + expect(out.error).toBe("Insufficient permissions."); + }); +}); diff --git a/new-deepnotes/apps/web/src/features/groups/group-members-detail.ts b/new-deepnotes/apps/web/src/features/groups/group-members-detail.ts new file mode 100644 index 00000000..0dbd8ea4 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/group-members-detail.ts @@ -0,0 +1,33 @@ +import type { DeepnotesApiClient } from "../../api/client"; +import type { components } from "../../api/api-types.generated"; + +export type GroupMembersDetail = components["schemas"]["GroupMembersDetailResponse"]; +export type GroupMemberRole = components["schemas"]["GroupMemberRole"]; + +function errorMessageFromResponse(body: unknown, fallback: string): string { + if ( + body && + typeof body === "object" && + "message" in body && + typeof (body as { message?: string }).message === "string" + ) { + return (body as { message: string }).message; + } + return fallback; +} + +export async function fetchGroupMembersDetail(input: { + client: DeepnotesApiClient; + groupId: string; +}): Promise<{ data: GroupMembersDetail | null; error: string | null }> { + const res = await input.client.GET("/api/groups/{groupId}/members/detail", { + params: { path: { groupId: input.groupId } }, + }); + if (res.response.status === 200 && res.data) { + return { data: res.data, error: null }; + } + return { + data: null, + error: errorMessageFromResponse(res.error, "Could not load group members."), + }; +} diff --git a/new-deepnotes/apps/web/src/features/groups/group-membership-crypto.ts b/new-deepnotes/apps/web/src/features/groups/group-membership-crypto.ts new file mode 100644 index 00000000..16667348 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/group-membership-crypto.ts @@ -0,0 +1,269 @@ +import { + base64ToBytes, + createKeyring, + createPrivateKeyring, + createSymmetricKeyring, + DataLayer, + ensureSodiumReady, + wrapKeyPair, + wrapSymmetricKey, + type KeyPair, +} from "@deepnotes/e2ee"; + +import { uint8ToBase64 } from "../auth/bytes"; +import type { StoredSessionCrypto } from "../auth/crypto-storage"; + +export type InviteCryptoBootstrapJson = { + groupPublicKeyring: string; + groupAccessKeyring: string | null; + memberEncryptedAccessKeyring: string | null; + memberEncryptedInternalKeyring: string; + notificationRecipientPublicKeyrings?: { + userId: string; + publicKeyring: string; + }[]; +}; + +function textToBytes(s: string): Uint8Array { + return new TextEncoder().encode(s); +} + +function pickAccessKeyringBytes(input: { + memberB64: string | null; + groupB64: string | null; +}): Uint8Array { + if (input.memberB64 != null && input.memberB64.length > 0) { + return base64ToBytes(input.memberB64); + } + if (input.groupB64 != null && input.groupB64.length > 0) { + return base64ToBytes(input.groupB64); + } + throw new Error("No group access key material."); +} + +async function unlockSessionKeyPair( + stored: StoredSessionCrypto, +): Promise { + await ensureSodiumReady(); + const sessionKey = wrapSymmetricKey(base64ToBytes(stored.sessionKeyB64)); + const { userId } = stored; + const publicKeyring = createKeyring(base64ToBytes(stored.publicKeyringB64)); + const privateKeyring = createPrivateKeyring( + base64ToBytes(stored.encryptedPrivateKeyringB64), + ).unwrapSymmetric(sessionKey, { + associatedData: { + context: "SessionUserPrivateKeyring", + userId, + }, + }); + return wrapKeyPair(publicKeyring, privateKeyring); +} + +async function wrapGroupKeyringsForRecipientPublicKey(input: { + keyPair: KeyPair; + bootstrap: InviteCryptoBootstrapJson; + recipientPublicKeyringB64: string; + groupIsPublic: boolean; +}): Promise<{ + encryptedAccessKeyring?: string; + encryptedInternalKeyring: string; +}> { + await ensureSodiumReady(); + const { keyPair } = input; + const recipientPk = createKeyring( + base64ToBytes(input.recipientPublicKeyringB64), + ); + + let ir = createSymmetricKeyring( + base64ToBytes(input.bootstrap.memberEncryptedInternalKeyring), + ); + if (ir.topLayer === DataLayer.Asymmetric) { + ir = ir.unwrapAsymmetric(keyPair.privateKey); + } + if (ir.topLayer !== DataLayer.Raw) { + throw new Error( + "Could not unwrap group internal keyring (unsupported lock).", + ); + } + const encInternal = uint8ToBase64( + ir.wrapAsymmetric(keyPair, recipientPk).wrappedValue, + ); + + let encAccess: string | undefined; + if (!input.groupIsPublic) { + const accessBytes = pickAccessKeyringBytes({ + memberB64: input.bootstrap.memberEncryptedAccessKeyring, + groupB64: input.bootstrap.groupAccessKeyring, + }); + let accessRing = createSymmetricKeyring(accessBytes); + if (accessRing.topLayer === DataLayer.Asymmetric) { + accessRing = accessRing.unwrapAsymmetric(keyPair.privateKey); + } + if (accessRing.topLayer !== DataLayer.Raw) { + throw new Error( + "Group access keyring is still locked (e.g. group password).", + ); + } + encAccess = uint8ToBase64( + accessRing.wrapAsymmetric(keyPair, recipientPk).wrappedValue, + ); + } + + return { + encryptedAccessKeyring: encAccess, + encryptedInternalKeyring: encInternal, + }; +} + +/** + * Raw `groups.access_keyring` bytes for `POST โ€ฆ/privacy/public` (legacy `accessKeyring.wrappedValue`). + */ +export async function buildMakePublicAccessKeyringB64(input: { + stored: StoredSessionCrypto; + bootstrap: InviteCryptoBootstrapJson; +}): Promise { + await ensureSodiumReady(); + const keyPair = await unlockSessionKeyPair(input.stored); + const accessBytes = pickAccessKeyringBytes({ + memberB64: input.bootstrap.memberEncryptedAccessKeyring, + groupB64: input.bootstrap.groupAccessKeyring, + }); + let accessRing = createSymmetricKeyring(accessBytes); + if (accessRing.topLayer === DataLayer.Asymmetric) { + accessRing = accessRing.unwrapAsymmetric(keyPair.privateKey); + } + if (accessRing.topLayer !== DataLayer.Raw) { + throw new Error( + "Group access keyring is still locked (e.g. group password). Public transition is not available until unlock is implemented.", + ); + } + return uint8ToBase64(accessRing.wrappedValue); +} + +/** + * Ciphertexts for `POST /api/groups/{groupId}/join-invitations` (manager). + */ +export async function buildJoinInvitationSendBodies(input: { + stored: StoredSessionCrypto; + bootstrap: InviteCryptoBootstrapJson; + inviteePublicKeyringB64: string; + /** Display name for the invitee as stored on the group row. */ + inviteeDisplayName: string; + groupIsPublic: boolean; +}): Promise<{ + encryptedAccessKeyring?: string; + encryptedInternalKeyring: string; + userEncryptedName: string; + userEncryptedNameForUser: string; +}> { + await ensureSodiumReady(); + const keyPair = await unlockSessionKeyPair(input.stored); + const inviteePk = createKeyring( + base64ToBytes(input.inviteePublicKeyringB64), + ); + const groupPk = createKeyring(base64ToBytes(input.bootstrap.groupPublicKeyring)); + + const wrapped = await wrapGroupKeyringsForRecipientPublicKey({ + keyPair, + bootstrap: input.bootstrap, + recipientPublicKeyringB64: input.inviteePublicKeyringB64, + groupIsPublic: input.groupIsPublic, + }); + + const nameBytes = textToBytes(input.inviteeDisplayName); + const userEncryptedName = uint8ToBase64( + keyPair.encrypt(nameBytes, groupPk, { padding: true }), + ); + const userEncryptedNameForUser = uint8ToBase64( + keyPair.encrypt(nameBytes, inviteePk, { padding: true }), + ); + + return { + ...wrapped, + userEncryptedName, + userEncryptedNameForUser, + }; +} + +/** `POST โ€ฆ/join-invitations/me/accept` body. */ +export async function buildJoinInvitationAcceptBody(input: { + stored: StoredSessionCrypto; + groupPublicKeyringB64: string; + displayName: string; +}): Promise<{ userEncryptedName: string }> { + await ensureSodiumReady(); + const keyPair = await unlockSessionKeyPair(input.stored); + const groupPk = createKeyring(base64ToBytes(input.groupPublicKeyringB64)); + const userEncryptedName = uint8ToBase64( + keyPair.encrypt(textToBytes(input.displayName), groupPk, { + padding: true, + }), + ); + return { userEncryptedName }; +} + +/** `POST โ€ฆ/join-requests` body (requester). */ +export async function buildJoinRequestSendBodies(input: { + stored: StoredSessionCrypto; + groupPublicKeyringB64: string; + groupId: string; + displayName: string; +}): Promise<{ + encryptedUserName: string; + encryptedUserNameForUser: string; +}> { + await ensureSodiumReady(); + const keyPair = await unlockSessionKeyPair(input.stored); + const sessionKey = wrapSymmetricKey(base64ToBytes(input.stored.sessionKeyB64)); + const { userId } = input.stored; + + const groupPk = createKeyring(base64ToBytes(input.groupPublicKeyringB64)); + const nameBytes = textToBytes(input.displayName); + const encryptedUserName = uint8ToBase64( + keyPair.encrypt(nameBytes, groupPk, { padding: true }), + ); + + const userSym = createSymmetricKeyring( + base64ToBytes(input.stored.encryptedSymmetricKeyringB64), + ).unwrapSymmetric(sessionKey, { + associatedData: { + context: "SessionUserSymmetricKeyring", + userId, + }, + }); + if (userSym.topLayer !== DataLayer.Raw) { + throw new Error("User symmetric keyring is not unlocked."); + } + const encryptedUserNameForUser = uint8ToBase64( + userSym.encrypt(nameBytes, { + padding: true, + associatedData: { + context: "GroupJoinRequestUserNameForUser", + groupId: input.groupId, + userId, + }, + }), + ); + + return { encryptedUserName, encryptedUserNameForUser }; +} + +/** `POST โ€ฆ/join-requests/{userId}/accept` ciphertexts (manager). */ +export async function buildJoinRequestAcceptBodies(input: { + stored: StoredSessionCrypto; + bootstrap: InviteCryptoBootstrapJson; + requesterPublicKeyringB64: string; + groupIsPublic: boolean; +}): Promise<{ + encryptedAccessKeyring?: string; + encryptedInternalKeyring: string; +}> { + await ensureSodiumReady(); + const keyPair = await unlockSessionKeyPair(input.stored); + return wrapGroupKeyringsForRecipientPublicKey({ + keyPair, + bootstrap: input.bootstrap, + recipientPublicKeyringB64: input.requesterPublicKeyringB64, + groupIsPublic: input.groupIsPublic, + }); +} diff --git a/new-deepnotes/apps/web/src/features/groups/group-notification-crypto.ts b/new-deepnotes/apps/web/src/features/groups/group-notification-crypto.ts new file mode 100644 index 00000000..de4c03fe --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/group-notification-crypto.ts @@ -0,0 +1,170 @@ +import { + base64ToBytes, + createKeyring, + createPrivateKeyring, + ensureSodiumReady, + wrapKeyPair, + wrapSymmetricKey, + type KeyPair, +} from "@deepnotes/e2ee"; +import { pack } from "msgpackr"; + +import { uint8ToBase64 } from "../auth/bytes"; +import type { StoredSessionCrypto } from "../auth/crypto-storage"; + +async function unlockSessionKeyPair(stored: StoredSessionCrypto): Promise { + await ensureSodiumReady(); + const sessionKey = wrapSymmetricKey(base64ToBytes(stored.sessionKeyB64)); + const { userId } = stored; + const publicKeyring = createKeyring(base64ToBytes(stored.publicKeyringB64)); + const privateKeyring = createPrivateKeyring( + base64ToBytes(stored.encryptedPrivateKeyringB64), + ).unwrapSymmetric(sessionKey, { + associatedData: { + context: "SessionUserPrivateKeyring", + userId, + }, + }); + return wrapKeyPair(publicKeyring, privateKeyring); +} + +/** + * Legacy `createNotifications` for `group-invitation-sent` (three audience buckets). + * Returns bodies for `POST โ€ฆ/join-invitations` `notifications` array (base64 fields). + */ +export async function buildGroupInviteSentNotifications(input: { + stored: StoredSessionCrypto; + agentUserId: string; + inviteeUserId: string; + groupId: string; + inviteeDisplayName: string; + /** Until group name is exposed on `/me`, defaults are placeholders. */ + groupName?: string; + agentName?: string; + recipientPublicKeyrings: { userId: string; publicKeyring: Uint8Array }[]; +}): Promise< + Array<{ + type: "group-invitation-sent"; + encryptedContent: string; + recipients: Record; + }> +> { + await ensureSodiumReady(); + const keyPair = await unlockSessionKeyPair(input.stored); + + const recipients: Record = {}; + for (const r of input.recipientPublicKeyrings) { + recipients[r.userId] = { publicKeyring: r.publicKeyring }; + } + + const groupName = input.groupName ?? "Group"; + const agentName = input.agentName ?? "Someone"; + + const agentSymmetricKey = wrapSymmetricKey(); + const targetSymmetricKey = wrapSymmetricKey(); + const observersSymmetricKey = wrapSymmetricKey(); + + const out: Array<{ + type: "group-invitation-sent"; + encryptedContent: string; + recipients: Record; + }> = []; + + { + const recipientsEnc = { + [input.agentUserId]: { + encryptedSymmetricKey: uint8ToBase64( + keyPair.encrypt(agentSymmetricKey.value, keyPair.publicKey, { + padding: true, + }), + ), + }, + }; + out.push({ + type: "group-invitation-sent", + recipients: recipientsEnc, + encryptedContent: uint8ToBase64( + agentSymmetricKey.encrypt( + pack({ + groupId: input.groupId, + patientId: input.inviteeUserId, + groupName, + targetName: input.inviteeDisplayName, + recipientType: "agent", + }), + { + padding: true, + associatedData: { context: "UserNotificationContent" }, + }, + ), + ), + }); + } + + if (recipients[input.inviteeUserId] != null) { + const inviteePk = createKeyring(recipients[input.inviteeUserId]!.publicKeyring); + out.push({ + type: "group-invitation-sent", + recipients: { + [input.inviteeUserId]: { + encryptedSymmetricKey: uint8ToBase64( + keyPair.encrypt(targetSymmetricKey.value, inviteePk, { + padding: true, + }), + ), + }, + }, + encryptedContent: uint8ToBase64( + targetSymmetricKey.encrypt( + pack({ + groupId: input.groupId, + groupName, + recipientType: "target", + }), + { + padding: true, + associatedData: { context: "UserNotificationContent" }, + }, + ), + ), + }); + } + + const observerRecipientEntries = Object.entries(recipients).filter( + ([uid]) => uid !== input.agentUserId && uid !== input.inviteeUserId, + ); + if (observerRecipientEntries.length > 0) { + const obsRecipients: Record = {}; + for (const [recipientUserId, { publicKeyring }] of observerRecipientEntries) { + const pk = createKeyring(publicKeyring); + obsRecipients[recipientUserId] = { + encryptedSymmetricKey: uint8ToBase64( + keyPair.encrypt(observersSymmetricKey.value, pk, { + padding: true, + }), + ), + }; + } + out.push({ + type: "group-invitation-sent", + recipients: obsRecipients, + encryptedContent: uint8ToBase64( + observersSymmetricKey.encrypt( + pack({ + groupId: input.groupId, + groupName, + agentName, + targetName: input.inviteeDisplayName, + recipientType: "observer", + }), + { + padding: true, + associatedData: { context: "UserNotificationContent" }, + }, + ), + ), + }); + } + + return out; +} diff --git a/new-deepnotes/apps/web/src/features/groups/group-password-crypto.change.test.ts b/new-deepnotes/apps/web/src/features/groups/group-password-crypto.change.test.ts new file mode 100644 index 00000000..eb5b29e3 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/group-password-crypto.change.test.ts @@ -0,0 +1,65 @@ +import { + createSymmetricKeyring, + DataLayer, + deriveGroupPasswordValues, +} from "@deepnotes/e2ee"; +import { describe, expect, it } from "vitest"; + +import { readSessionCrypto } from "../auth/crypto-storage"; +import { buildGroupPasswordChangeRequestBody } from "./group-password-crypto"; +import { setupSession } from "./group-password-crypto.setup"; + +describe("group-password-crypto change", () => { + it("buildGroupPasswordChangeRequestBody re-wraps with new password key", async () => { + const groupId = "g123456789012345678901"; + const userId = "u123456789012345678901"; + const oldPassword = "old-password-123"; + const newPassword = "new-password-456"; + + const { keyPair } = await setupSession(groupId, userId); + + const accessKeyring = createSymmetricKeyring(); + const groupContentKeyring = createSymmetricKeyring(); + + const memberEncryptedAccessKeyring = accessKeyring + .wrapAsymmetric(keyPair, keyPair.publicKey) + .wrappedValue; + + const { passwordKey: oldPasswordKey } = deriveGroupPasswordValues(groupId, oldPassword); + const groupEncryptedContentKeyring = groupContentKeyring + .wrapSymmetric(oldPasswordKey, { + associatedData: { context: "GroupContentKeyringPasswordProtection", groupId }, + }) + .wrapSymmetric(accessKeyring, { + associatedData: { context: "GroupContentKeyring", groupId }, + }).wrappedValue; + + const body = await buildGroupPasswordChangeRequestBody({ + groupId, + currentPassword: oldPassword, + newPassword, + groupEncryptedContentKeyring, + memberEncryptedAccessKeyring, + groupAccessKeyring: null, + stored: readSessionCrypto()!, + }); + + expect(body.groupCurrentPasswordHash).toBeTruthy(); + expect(body.groupNewPasswordHash).toBeTruthy(); + expect(body.groupEncryptedContentKeyring).toBeTruthy(); + + // Verify with new password + const { passwordKey: newPasswordKey } = deriveGroupPasswordValues(groupId, newPassword); + let unlocked = createSymmetricKeyring( + new Uint8Array(Buffer.from(body.groupEncryptedContentKeyring, "base64")), + ); + unlocked = unlocked.unwrapSymmetric(accessKeyring, { + associatedData: { context: "GroupContentKeyring", groupId }, + }); + unlocked = unlocked.unwrapSymmetric(newPasswordKey, { + associatedData: { context: "GroupContentKeyringPasswordProtection", groupId }, + }); + expect(unlocked.topLayer).toBe(DataLayer.Raw); + expect(unlocked.value).toEqual(groupContentKeyring.value); + }); +}); diff --git a/new-deepnotes/apps/web/src/features/groups/group-password-crypto.disable.test.ts b/new-deepnotes/apps/web/src/features/groups/group-password-crypto.disable.test.ts new file mode 100644 index 00000000..a858aa6f --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/group-password-crypto.disable.test.ts @@ -0,0 +1,58 @@ +import { + createSymmetricKeyring, + DataLayer, + deriveGroupPasswordValues, +} from "@deepnotes/e2ee"; +import { describe, expect, it } from "vitest"; + +import { readSessionCrypto } from "../auth/crypto-storage"; +import { buildGroupPasswordDisableRequestBody } from "./group-password-crypto"; +import { setupSession } from "./group-password-crypto.setup"; + +describe("group-password-crypto disable", () => { + it("buildGroupPasswordDisableRequestBody removes password layer", async () => { + const groupId = "g123456789012345678902"; + const userId = "u123456789012345678902"; + const password = "disable-me-789"; + + const { keyPair } = await setupSession(groupId, userId); + + const accessKeyring = createSymmetricKeyring(); + const groupContentKeyring = createSymmetricKeyring(); + + const memberEncryptedAccessKeyring = accessKeyring + .wrapAsymmetric(keyPair, keyPair.publicKey) + .wrappedValue; + + const { passwordKey } = deriveGroupPasswordValues(groupId, password); + const groupEncryptedContentKeyring = groupContentKeyring + .wrapSymmetric(passwordKey, { + associatedData: { context: "GroupContentKeyringPasswordProtection", groupId }, + }) + .wrapSymmetric(accessKeyring, { + associatedData: { context: "GroupContentKeyring", groupId }, + }).wrappedValue; + + const body = await buildGroupPasswordDisableRequestBody({ + groupId, + currentPassword: password, + groupEncryptedContentKeyring, + memberEncryptedAccessKeyring, + groupAccessKeyring: null, + stored: readSessionCrypto()!, + }); + + expect(body.groupPasswordHash).toBeTruthy(); + expect(body.groupEncryptedContentKeyring).toBeTruthy(); + + // Verify no password layer remains (only access-wrapped raw) + let unlocked = createSymmetricKeyring( + new Uint8Array(Buffer.from(body.groupEncryptedContentKeyring, "base64")), + ); + unlocked = unlocked.unwrapSymmetric(accessKeyring, { + associatedData: { context: "GroupContentKeyring", groupId }, + }); + expect(unlocked.topLayer).toBe(DataLayer.Raw); + expect(unlocked.value).toEqual(groupContentKeyring.value); + }); +}); diff --git a/new-deepnotes/apps/web/src/features/groups/group-password-crypto.enable.test.ts b/new-deepnotes/apps/web/src/features/groups/group-password-crypto.enable.test.ts new file mode 100644 index 00000000..510b8264 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/group-password-crypto.enable.test.ts @@ -0,0 +1,58 @@ +import { + createSymmetricKeyring, + DataLayer, + deriveGroupPasswordValues, +} from "@deepnotes/e2ee"; +import { describe, expect, it } from "vitest"; + +import { readSessionCrypto } from "../auth/crypto-storage"; +import { buildGroupPasswordEnableRequestBody } from "./group-password-crypto"; +import { setupSession } from "./group-password-crypto.setup"; + +describe("group-password-crypto enable", () => { + it("buildGroupPasswordEnableRequestBody wraps raw content with password key", async () => { + const groupId = "g12345678901234567890"; + const userId = "u12345678901234567890"; + const password = "secret-group-password"; + + const { keyPair } = await setupSession(groupId, userId); + + const accessKeyring = createSymmetricKeyring(); + const groupContentKeyring = createSymmetricKeyring(); + + const memberEncryptedAccessKeyring = accessKeyring + .wrapAsymmetric(keyPair, keyPair.publicKey) + .wrappedValue; + + const groupEncryptedContentKeyring = groupContentKeyring + .wrapSymmetric(accessKeyring, { + associatedData: { context: "GroupContentKeyring", groupId }, + }).wrappedValue; + + const body = await buildGroupPasswordEnableRequestBody({ + groupId, + password, + groupEncryptedContentKeyring, + memberEncryptedAccessKeyring, + groupAccessKeyring: null, + stored: readSessionCrypto()!, + }); + + expect(body.groupPasswordHash).toBeTruthy(); + expect(body.groupEncryptedContentKeyring).toBeTruthy(); + + // Verify the produced keyring can be unwrapped + const { passwordKey } = deriveGroupPasswordValues(groupId, password); + let unlocked = createSymmetricKeyring( + new Uint8Array(Buffer.from(body.groupEncryptedContentKeyring, "base64")), + ); + unlocked = unlocked.unwrapSymmetric(accessKeyring, { + associatedData: { context: "GroupContentKeyring", groupId }, + }); + unlocked = unlocked.unwrapSymmetric(passwordKey, { + associatedData: { context: "GroupContentKeyringPasswordProtection", groupId }, + }); + expect(unlocked.topLayer).toBe(DataLayer.Raw); + expect(unlocked.value).toEqual(groupContentKeyring.value); + }); +}); diff --git a/new-deepnotes/apps/web/src/features/groups/group-password-crypto.setup.ts b/new-deepnotes/apps/web/src/features/groups/group-password-crypto.setup.ts new file mode 100644 index 00000000..6c33a6c6 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/group-password-crypto.setup.ts @@ -0,0 +1,46 @@ +import { randomBytes } from "node:crypto"; + +import { + bytesToBase64, + createKeyring, + createPrivateKeyring, + createSymmetricKeyring, + ensureSodiumReady, + wrapKeyPair, + wrapSymmetricKey, +} from "@deepnotes/e2ee"; + +import { uint8ToBase64 } from "../auth/bytes"; +import { clearSessionCrypto, writeSessionCrypto } from "../auth/crypto-storage"; + +export async function setupSession(groupId: string, userId: string) { + await ensureSodiumReady(); + clearSessionCrypto(); + + const sessionKey = wrapSymmetricKey(randomBytes(32)); + const publicKeyring = createKeyring(randomBytes(32)); + const privateKeyring = createPrivateKeyring(randomBytes(32)); + const keyPair = wrapKeyPair(publicKeyring, privateKeyring); + + const userEncryptedPrivateKeyring = privateKeyring + .wrapSymmetric(sessionKey, { + associatedData: { context: "SessionUserPrivateKeyring", userId }, + }) + .wrappedValue; + + writeSessionCrypto({ + userId, + sessionId: "s12345678901234567890", + sessionKeyB64: bytesToBase64(sessionKey.value), + publicKeyringB64: bytesToBase64(publicKeyring.wrappedValue), + encryptedPrivateKeyringB64: uint8ToBase64(userEncryptedPrivateKeyring), + encryptedSymmetricKeyringB64: uint8ToBase64( + createSymmetricKeyring().wrapSymmetric(sessionKey, { + associatedData: { context: "SessionUserSymmetricKeyring", userId }, + }).wrappedValue, + ), + personalGroupId: groupId, + }); + + return { keyPair }; +} diff --git a/new-deepnotes/apps/web/src/features/groups/group-password-crypto.ts b/new-deepnotes/apps/web/src/features/groups/group-password-crypto.ts new file mode 100644 index 00000000..c7d398db --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/group-password-crypto.ts @@ -0,0 +1,254 @@ +import { + base64ToBytes, + createKeyring, + createPrivateKeyring, + createSymmetricKeyring, + DataLayer, + deriveGroupPasswordValues, + ensureSodiumReady, + wrapKeyPair, + wrapSymmetricKey, +} from "@deepnotes/e2ee"; + +import { uint8ToBase64 } from "../auth/bytes"; +import type { StoredSessionCrypto } from "../auth/crypto-storage"; + +function pickAccessKeyringBytes(input: { + member: Uint8Array | null; + group: Uint8Array | null; +}): Uint8Array { + if (input.member != null && input.member.byteLength > 0) { + return input.member; + } + if (input.group != null && input.group.byteLength > 0) { + return input.group; + } + throw new Error("No group access key material."); +} + +async function unlockAccessKeyring(input: { + memberEncryptedAccessKeyring: Uint8Array | null; + groupAccessKeyring: Uint8Array | null; + stored: StoredSessionCrypto; +}) { + await ensureSodiumReady(); + const sessionKey = wrapSymmetricKey( + base64ToBytes(input.stored.sessionKeyB64), + ); + const { userId } = input.stored; + + const publicKeyring = createKeyring( + base64ToBytes(input.stored.publicKeyringB64), + ); + const privateKeyring = createPrivateKeyring( + base64ToBytes(input.stored.encryptedPrivateKeyringB64), + ).unwrapSymmetric(sessionKey, { + associatedData: { + context: "SessionUserPrivateKeyring", + userId, + }, + }); + const keyPair = wrapKeyPair(publicKeyring, privateKeyring); + + const accessBytes = pickAccessKeyringBytes({ + member: input.memberEncryptedAccessKeyring, + group: input.groupAccessKeyring, + }); + + let accessRing = createSymmetricKeyring(accessBytes); + if (accessRing.topLayer === DataLayer.Asymmetric) { + accessRing = accessRing.unwrapAsymmetric(keyPair.privateKey); + } + if (accessRing.topLayer !== DataLayer.Raw) { + throw new Error( + "Group access keyring is still locked (e.g. group password).", + ); + } + return accessRing; +} + +export async function buildGroupPasswordEnableRequestBody(input: { + groupId: string; + password: string; + groupEncryptedContentKeyring: Uint8Array; + memberEncryptedAccessKeyring: Uint8Array | null; + groupAccessKeyring: Uint8Array | null; + stored: StoredSessionCrypto; +}): Promise<{ + groupPasswordHash: string; + groupEncryptedContentKeyring: string; +}> { + await ensureSodiumReady(); + const { passwordHash, passwordKey } = deriveGroupPasswordValues( + input.groupId, + input.password, + ); + + const accessRing = await unlockAccessKeyring({ + memberEncryptedAccessKeyring: input.memberEncryptedAccessKeyring, + groupAccessKeyring: input.groupAccessKeyring, + stored: input.stored, + }); + + let rawContent = createSymmetricKeyring(input.groupEncryptedContentKeyring); + if (rawContent.topLayer === DataLayer.Symmetric) { + rawContent = rawContent.unwrapSymmetric(accessRing, { + associatedData: { + context: "GroupContentKeyring", + groupId: input.groupId, + }, + }); + } + if (rawContent.topLayer !== DataLayer.Raw) { + throw new Error( + "Group content keyring is already password-protected or corrupted.", + ); + } + + const passwordWrapped = rawContent.wrapSymmetric(passwordKey, { + associatedData: { + context: "GroupContentKeyringPasswordProtection", + groupId: input.groupId, + }, + }); + + const final = passwordWrapped.wrapSymmetric(accessRing, { + associatedData: { + context: "GroupContentKeyring", + groupId: input.groupId, + }, + }); + + return { + groupPasswordHash: uint8ToBase64(passwordHash), + groupEncryptedContentKeyring: uint8ToBase64(final.wrappedValue), + }; +} + +export async function buildGroupPasswordChangeRequestBody(input: { + groupId: string; + currentPassword: string; + newPassword: string; + groupEncryptedContentKeyring: Uint8Array; + memberEncryptedAccessKeyring: Uint8Array | null; + groupAccessKeyring: Uint8Array | null; + stored: StoredSessionCrypto; +}): Promise<{ + groupCurrentPasswordHash: string; + groupNewPasswordHash: string; + groupEncryptedContentKeyring: string; +}> { + await ensureSodiumReady(); + const { passwordHash: currentHash, passwordKey: currentKey } = + deriveGroupPasswordValues(input.groupId, input.currentPassword); + const { passwordHash: newHash, passwordKey: newKey } = + deriveGroupPasswordValues(input.groupId, input.newPassword); + + const accessRing = await unlockAccessKeyring({ + memberEncryptedAccessKeyring: input.memberEncryptedAccessKeyring, + groupAccessKeyring: input.groupAccessKeyring, + stored: input.stored, + }); + + let rawContent = createSymmetricKeyring(input.groupEncryptedContentKeyring); + if (rawContent.topLayer === DataLayer.Symmetric) { + rawContent = rawContent.unwrapSymmetric(accessRing, { + associatedData: { + context: "GroupContentKeyring", + groupId: input.groupId, + }, + }); + } + if (rawContent.topLayer === DataLayer.Symmetric) { + rawContent = rawContent.unwrapSymmetric(currentKey, { + associatedData: { + context: "GroupContentKeyringPasswordProtection", + groupId: input.groupId, + }, + }); + } + if (rawContent.topLayer !== DataLayer.Raw) { + throw new Error( + "Group content keyring could not be unwrapped with current password.", + ); + } + + const passwordWrapped = rawContent.wrapSymmetric(newKey, { + associatedData: { + context: "GroupContentKeyringPasswordProtection", + groupId: input.groupId, + }, + }); + + const final = passwordWrapped.wrapSymmetric(accessRing, { + associatedData: { + context: "GroupContentKeyring", + groupId: input.groupId, + }, + }); + + return { + groupCurrentPasswordHash: uint8ToBase64(currentHash), + groupNewPasswordHash: uint8ToBase64(newHash), + groupEncryptedContentKeyring: uint8ToBase64(final.wrappedValue), + }; +} + +export async function buildGroupPasswordDisableRequestBody(input: { + groupId: string; + currentPassword: string; + groupEncryptedContentKeyring: Uint8Array; + memberEncryptedAccessKeyring: Uint8Array | null; + groupAccessKeyring: Uint8Array | null; + stored: StoredSessionCrypto; +}): Promise<{ + groupPasswordHash: string; + groupEncryptedContentKeyring: string; +}> { + await ensureSodiumReady(); + const { passwordHash, passwordKey } = deriveGroupPasswordValues( + input.groupId, + input.currentPassword, + ); + + const accessRing = await unlockAccessKeyring({ + memberEncryptedAccessKeyring: input.memberEncryptedAccessKeyring, + groupAccessKeyring: input.groupAccessKeyring, + stored: input.stored, + }); + + let rawContent = createSymmetricKeyring(input.groupEncryptedContentKeyring); + if (rawContent.topLayer === DataLayer.Symmetric) { + rawContent = rawContent.unwrapSymmetric(accessRing, { + associatedData: { + context: "GroupContentKeyring", + groupId: input.groupId, + }, + }); + } + if (rawContent.topLayer === DataLayer.Symmetric) { + rawContent = rawContent.unwrapSymmetric(passwordKey, { + associatedData: { + context: "GroupContentKeyringPasswordProtection", + groupId: input.groupId, + }, + }); + } + if (rawContent.topLayer !== DataLayer.Raw) { + throw new Error( + "Group content keyring could not be unwrapped with current password.", + ); + } + + const final = rawContent.wrapSymmetric(accessRing, { + associatedData: { + context: "GroupContentKeyring", + groupId: input.groupId, + }, + }); + + return { + groupPasswordHash: uint8ToBase64(passwordHash), + groupEncryptedContentKeyring: uint8ToBase64(final.wrappedValue), + }; +} diff --git a/new-deepnotes/apps/web/src/features/groups/group-role-policy.test.ts b/new-deepnotes/apps/web/src/features/groups/group-role-policy.test.ts new file mode 100644 index 00000000..9d0cd4a2 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/group-role-policy.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; + +import { + canChangeRole, + canEditGroupSettings, + canManageRole, + roleHasManageLowerRanks, +} from "./group-role-policy"; + +describe("group-role-policy", () => { + it("owner can manage lower ranks", () => { + expect(canManageRole("owner", "member")).toBe(true); + expect(canManageRole("owner", "owner")).toBe(true); + }); + + it("moderator can manage member but not admin", () => { + expect(canManageRole("moderator", "member")).toBe(true); + expect(canManageRole("moderator", "admin")).toBe(false); + }); + + it("canChangeRole requires both transitions", () => { + expect(canChangeRole("owner", "member", "viewer")).toBe(true); + expect(canChangeRole("moderator", "admin", "member")).toBe(false); + }); + + it("roleHasManageLowerRanks", () => { + expect(roleHasManageLowerRanks("moderator")).toBe(true); + expect(roleHasManageLowerRanks("member")).toBe(false); + }); + + it("canEditGroupSettings matches owner and admin only", () => { + expect(canEditGroupSettings("owner")).toBe(true); + expect(canEditGroupSettings("admin")).toBe(true); + expect(canEditGroupSettings("moderator")).toBe(false); + expect(canEditGroupSettings("member")).toBe(false); + }); +}); diff --git a/new-deepnotes/apps/web/src/features/groups/group-role-policy.ts b/new-deepnotes/apps/web/src/features/groups/group-role-policy.ts new file mode 100644 index 00000000..06ae24ce --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/group-role-policy.ts @@ -0,0 +1,48 @@ +/** + * Mirrors `@deepnotes/session` `group-role-ranks` for UI gating (no server imports in the bundle). + */ + +const RANK: Record = { + owner: 5, + admin: 4, + moderator: 3, + member: 2, + viewer: 1, +}; + +export function canManageRole(managerRole: string, targetRole: string): boolean { + const mr = RANK[managerRole]; + const tr = RANK[targetRole]; + if (mr == null || tr == null) { + return false; + } + const manageLower = ["owner", "admin", "moderator"].includes(managerRole); + const manageOwn = ["owner", "admin"].includes(managerRole); + if (tr < mr) { + return manageLower; + } + if (tr <= mr) { + return manageOwn; + } + return false; +} + +export function canChangeRole( + managerRole: string, + targetOldRole: string, + targetNewRole: string, +): boolean { + return ( + canManageRole(managerRole, targetOldRole) && + canManageRole(managerRole, targetNewRole) + ); +} + +export function roleHasManageLowerRanks(role: string): boolean { + return ["owner", "admin", "moderator"].includes(role); +} + +/** Matches session `editGroupSettings` (owners and admins). */ +export function canEditGroupSettings(viewerRole: string): boolean { + return ["owner", "admin"].includes(viewerRole); +} diff --git a/new-deepnotes/apps/web/src/features/groups/groups-overview.test.ts b/new-deepnotes/apps/web/src/features/groups/groups-overview.test.ts new file mode 100644 index 00000000..a2027a1b --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/groups-overview.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it, vi } from "vitest"; + +import type { DeepnotesApiClient } from "../../api/client"; +import { fetchGroupsOverview } from "./groups-overview"; + +describe("fetchGroupsOverview", () => { + it("aggregates main page, members, and first page window per group", async () => { + const client = { + GET: vi + .fn() + .mockResolvedValueOnce({ + response: { status: 200 }, + data: { groupIds: ["g1", "g2"] }, + }) + .mockResolvedValueOnce({ + response: { status: 200 }, + data: { mainPageId: "p0" }, + }) + .mockResolvedValueOnce({ + response: { status: 200 }, + data: { userIds: ["u1"] }, + }) + .mockResolvedValueOnce({ + response: { status: 200 }, + data: { pageIds: ["p0"], hasMore: false }, + }) + .mockResolvedValueOnce({ + response: { status: 200 }, + data: { mainPageId: "px" }, + }) + .mockResolvedValueOnce({ + response: { status: 404 }, + data: undefined, + }) + .mockResolvedValueOnce({ + response: { status: 200 }, + data: { pageIds: [], hasMore: false }, + }), + }; + const out = await fetchGroupsOverview({ + client: client as unknown as DeepnotesApiClient, + personalGroupId: "g1", + }); + expect(out.error).toBeNull(); + expect(out.rows).toHaveLength(2); + expect(out.rows[0]).toMatchObject({ + groupId: "g1", + isPersonal: true, + mainPageId: "p0", + memberUserCount: 1, + membersUnavailable: false, + pageIds: ["p0"], + }); + expect(out.rows[1]).toMatchObject({ + groupId: "g2", + isPersonal: false, + mainPageId: "px", + memberUserCount: null, + membersUnavailable: true, + }); + }); + + it("returns an error when me/groups fails", async () => { + const client = { + GET: vi.fn().mockResolvedValue({ + response: { status: 401 }, + error: { message: "nope" }, + data: undefined, + }), + }; + const out = await fetchGroupsOverview({ + client: client as unknown as DeepnotesApiClient, + personalGroupId: null, + }); + expect(out.rows).toEqual([]); + expect(out.error).toBe("nope"); + }); +}); diff --git a/new-deepnotes/apps/web/src/features/groups/groups-overview.ts b/new-deepnotes/apps/web/src/features/groups/groups-overview.ts new file mode 100644 index 00000000..ab9c360a --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/groups-overview.ts @@ -0,0 +1,90 @@ +import type { DeepnotesApiClient } from "../../api/client"; + +export type GroupOverviewRow = { + groupId: string; + /** `true` when this id matches the signed-in user's personal group. */ + isPersonal: boolean; + mainPageId: string | null; + memberUserCount: number | null; + /** Members list not available (403/404) โ€” e.g. stricter than pages on some edges. */ + membersUnavailable: boolean; + pageIds: string[]; + pagesHasMore: boolean; +}; + +/** + * Lists the caller's groups with main page id, member/user-id count, and first + * page id window (same 20-item cap as home). + */ +export async function fetchGroupsOverview(input: { + client: DeepnotesApiClient; + personalGroupId: string | null; +}): Promise<{ rows: GroupOverviewRow[]; error: string | null }> { + const { client, personalGroupId } = input; + const gRes = await client.GET("/api/users/me/groups", {}); + if (gRes.response.status !== 200 || !gRes.data) { + if (gRes.error && typeof gRes.error === "object" && "message" in gRes.error) { + return { + rows: [], + error: String((gRes.error as { message?: string }).message), + }; + } + return { rows: [], error: "Could not list groups." }; + } + + const groupIds = gRes.data.groupIds; + const rows = await Promise.all( + groupIds.map(async (groupId): Promise => { + const isPersonal = personalGroupId != null && groupId === personalGroupId; + + const [mainRes, membersRes, pagesRes] = await Promise.all([ + client.GET("/api/groups/{groupId}/main-page", { + params: { path: { groupId } }, + }), + client.GET("/api/groups/{groupId}/members", { + params: { path: { groupId } }, + }), + client.GET("/api/groups/{groupId}/pages", { + params: { path: { groupId } }, + }), + ]); + + const mainPageId = + mainRes.response.status === 200 && mainRes.data + ? mainRes.data.mainPageId + : null; + + let memberUserCount: number | null = null; + let membersUnavailable = false; + if (membersRes.response.status === 200 && membersRes.data) { + memberUserCount = membersRes.data.userIds.length; + } else if ( + membersRes.response.status === 403 || + membersRes.response.status === 404 + ) { + membersUnavailable = true; + } + + const pageIds = + pagesRes.response.status === 200 && pagesRes.data + ? pagesRes.data.pageIds + : []; + const pagesHasMore = + pagesRes.response.status === 200 && pagesRes.data + ? pagesRes.data.hasMore + : false; + + return { + groupId, + isPersonal, + mainPageId, + memberUserCount, + membersUnavailable, + pageIds, + pagesHasMore, + }; + }), + ); + + return { rows, error: null }; +} diff --git a/new-deepnotes/apps/web/src/features/groups/groups-routes.ts b/new-deepnotes/apps/web/src/features/groups/groups-routes.ts new file mode 100644 index 00000000..3aef414b --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/groups-routes.ts @@ -0,0 +1,24 @@ +import type { RouteRecordRaw } from "vue-router"; + +export const groupsRoutes: RouteRecordRaw[] = [ + { + path: "/groups/:groupId/invite", + name: "group-invite", + component: () => import("./GroupInviteLandingView.vue"), + }, + { + path: "/groups/:groupId/join", + name: "group-join-request", + component: () => import("./GroupJoinRequestView.vue"), + }, + { + path: "/groups/:groupId", + name: "group-detail", + component: () => import("./GroupDetailView.vue"), + }, + { + path: "/groups", + name: "groups", + component: () => import("./GroupsView.vue"), + }, +]; diff --git a/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-actions.ts b/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-actions.ts new file mode 100644 index 00000000..2e2b74a0 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-actions.ts @@ -0,0 +1,105 @@ +import type { Ref } from "vue"; + +import type { DeepnotesApiClient } from "../../api/client"; +import type { components } from "../../api/api-types.generated"; +import type { GroupMembersDetail } from "./group-members-detail"; + +type GroupMemberRole = components["schemas"]["GroupMemberRole"]; + +/** + * Basic group member actions: leave, remove, change role. + */ +export function useGroupMemberActions({ + client, + resolvedGroupId, + detail, + actionLoading, + error, + load, + user, +}: { + client: DeepnotesApiClient; + resolvedGroupId: () => string | null; + detail: Ref; + actionLoading: Ref; + error: Ref; + load: () => Promise; + user: Ref<{ userId: string } | null>; +}) { + async function leaveGroup() { + const id = resolvedGroupId(); + const uid = user.value?.userId; + if (id == null || uid == null) { + return; + } + actionLoading.value = true; + error.value = null; + try { + const res = await client.DELETE("/api/groups/{groupId}/members/{userId}", { + params: { path: { groupId: id, userId: uid } }, + }); + if (res.response.status !== 204) { + error.value = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not leave group."; + return; + } + error.value = null; + detail.value = null; + } finally { + actionLoading.value = false; + } + } + + async function removeMember(targetUserId: string) { + const id = resolvedGroupId(); + if (id == null) { + return; + } + actionLoading.value = true; + error.value = null; + try { + const res = await client.DELETE("/api/groups/{groupId}/members/{userId}", { + params: { path: { groupId: id, userId: targetUserId } }, + }); + if (res.response.status !== 204) { + error.value = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not remove member."; + return; + } + await load(); + } finally { + actionLoading.value = false; + } + } + + async function patchMemberRole(targetUserId: string, role: GroupMemberRole) { + const id = resolvedGroupId(); + if (id == null) { + return; + } + actionLoading.value = true; + error.value = null; + try { + const res = await client.PATCH("/api/groups/{groupId}/members/{userId}", { + params: { path: { groupId: id, userId: targetUserId } }, + body: { role }, + }); + if (res.response.status !== 204) { + error.value = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not change role."; + return; + } + await load(); + } finally { + actionLoading.value = false; + } + } + + return { leaveGroup, removeMember, patchMemberRole }; +} diff --git a/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-deletion.ts b/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-deletion.ts new file mode 100644 index 00000000..e7ba03b4 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-deletion.ts @@ -0,0 +1,71 @@ +import type { Ref } from "vue"; + +import type { DeepnotesApiClient } from "../../api/client"; +import type { GroupMembersDetail } from "./group-members-detail"; + +/** + * Group deletion actions: soft delete, purge. + */ +export function useGroupDeletionActions({ + client, + resolvedGroupId, + detail, + actionLoading, + error, +}: { + client: DeepnotesApiClient; + resolvedGroupId: () => string | null; + detail: Ref; + actionLoading: Ref; + error: Ref; +}) { + async function softDeleteGroup() { + const id = resolvedGroupId(); + if (id == null) { + return; + } + actionLoading.value = true; + error.value = null; + try { + const res = await client.DELETE("/api/groups/{groupId}", { + params: { path: { groupId: id } }, + }); + if (res.response.status !== 204) { + error.value = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not delete group."; + return; + } + detail.value = null; + } finally { + actionLoading.value = false; + } + } + + async function purgeGroup() { + const id = resolvedGroupId(); + if (id == null) { + return; + } + actionLoading.value = true; + error.value = null; + try { + const res = await client.POST("/api/groups/{groupId}/purge", { + params: { path: { groupId: id } }, + }); + if (res.response.status !== 204) { + error.value = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not purge group."; + return; + } + detail.value = null; + } finally { + actionLoading.value = false; + } + } + + return { softDeleteGroup, purgeGroup }; +} diff --git a/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-invitations.ts b/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-invitations.ts new file mode 100644 index 00000000..16bb6306 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-invitations.ts @@ -0,0 +1,211 @@ +import type { Ref } from "vue"; + +import { base64ToBytes } from "@deepnotes/e2ee"; + +import type { DeepnotesApiClient } from "../../api/client"; +import type { components } from "../../api/api-types.generated"; +import { readSessionCrypto } from "../auth/session-keyrings"; +import type { GroupMembersDetail } from "./group-members-detail"; +import { + buildJoinInvitationSendBodies, + type InviteCryptoBootstrapJson, +} from "./group-membership-crypto"; +import { buildGroupInviteSentNotifications } from "./group-notification-crypto"; + +type GroupMemberRole = components["schemas"]["GroupMemberRole"]; +type GroupInviteNotificationPayload = + components["schemas"]["GroupInviteNotificationPayload"]; + +/** + * Group invitation actions: send, cancel, reject. + */ +export function useGroupInvitationActions({ + client, + resolvedGroupId, + detail, + actionLoading, + error, + load, + user, +}: { + client: DeepnotesApiClient; + resolvedGroupId: () => string | null; + detail: Ref; + actionLoading: Ref; + error: Ref; + load: () => Promise; + user: Ref<{ userId: string } | null>; +}) { + async function fetchInviteBootstrap(inviteeUserId?: string): Promise< + | { ok: true; data: InviteCryptoBootstrapJson } + | { ok: false; error: string } + > { + const id = resolvedGroupId(); + if (id == null) { + return { ok: false, error: "Invalid group." }; + } + const res = await client.GET("/api/groups/{groupId}/invite-crypto-bootstrap", { + params: { + path: { groupId: id }, + query: + inviteeUserId != null && inviteeUserId !== "" + ? { inviteeUserId } + : {}, + }, + }); + if (res.response.status !== 200 || res.data == null) { + const msg = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not load invite crypto material."; + return { ok: false, error: msg }; + } + const d = res.data as InviteCryptoBootstrapJson; + return { ok: true, data: d }; + } + + async function sendJoinInvitation(input: { + inviteeUserId: string; + invitationRole: GroupMemberRole; + inviteeDisplayName: string; + }) { + const id = resolvedGroupId(); + const d = detail.value; + if (id == null || d == null) { + return; + } + const stored = readSessionCrypto(); + if (stored == null) { + error.value = + "Client crypto is not unlocked. Sign in with your account password on this device."; + return; + } + actionLoading.value = true; + error.value = null; + try { + const boot = await fetchInviteBootstrap(input.inviteeUserId); + if (!boot.ok) { + error.value = boot.error; + return; + } + const pkRes = await client.GET("/api/users/{userId}/public-keyring", { + params: { path: { userId: input.inviteeUserId } }, + }); + if (pkRes.response.status !== 200 || pkRes.data == null) { + error.value = + pkRes.error && + typeof pkRes.error === "object" && + "message" in pkRes.error + ? String((pkRes.error as { message?: string }).message) + : "Could not load invitee public key."; + return; + } + const bodies = await buildJoinInvitationSendBodies({ + stored, + bootstrap: boot.data, + inviteePublicKeyringB64: pkRes.data.publicKeyring, + inviteeDisplayName: input.inviteeDisplayName, + groupIsPublic: d.groupIsPublic, + }); + + const recipientPublicKeyrings = + boot.data.notificationRecipientPublicKeyrings?.map((r) => ({ + userId: r.userId, + publicKeyring: base64ToBytes(r.publicKeyring), + })) ?? []; + + let notifications: GroupInviteNotificationPayload[] | undefined; + if (recipientPublicKeyrings.length > 0 && user.value?.userId != null) { + notifications = await buildGroupInviteSentNotifications({ + stored, + agentUserId: user.value.userId, + inviteeUserId: input.inviteeUserId, + groupId: id, + inviteeDisplayName: input.inviteeDisplayName, + recipientPublicKeyrings, + }); + } + + const res = await client.POST("/api/groups/{groupId}/join-invitations", { + params: { path: { groupId: id } }, + body: { + inviteeUserId: input.inviteeUserId, + invitationRole: input.invitationRole, + encryptedInternalKeyring: bodies.encryptedInternalKeyring, + userEncryptedName: bodies.userEncryptedName, + userEncryptedNameForUser: bodies.userEncryptedNameForUser, + ...(bodies.encryptedAccessKeyring != null + ? { encryptedAccessKeyring: bodies.encryptedAccessKeyring } + : {}), + ...(notifications != null && notifications.length > 0 + ? { notifications } + : {}), + }, + }); + if (res.response.status !== 204) { + error.value = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not send invitation."; + return; + } + await load(); + } catch (e) { + error.value = e instanceof Error ? e.message : "Invitation failed."; + } finally { + actionLoading.value = false; + } + } + + async function cancelInvitation(inviteeUserId: string) { + const id = resolvedGroupId(); + if (id == null) { + return; + } + actionLoading.value = true; + error.value = null; + try { + const res = await client.DELETE( + "/api/groups/{groupId}/join-invitations/{userId}", + { params: { path: { groupId: id, userId: inviteeUserId } } }, + ); + if (res.response.status !== 204) { + error.value = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not cancel invitation."; + return; + } + await load(); + } finally { + actionLoading.value = false; + } + } + + async function rejectMyInvitation() { + const id = resolvedGroupId(); + if (id == null) { + return; + } + actionLoading.value = true; + error.value = null; + try { + const res = await client.POST( + "/api/groups/{groupId}/join-invitations/me/reject", + { params: { path: { groupId: id } } }, + ); + if (res.response.status !== 204) { + error.value = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not reject invitation."; + return; + } + await load(); + } finally { + actionLoading.value = false; + } + } + + return { sendJoinInvitation, cancelInvitation, rejectMyInvitation }; +} diff --git a/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-join-requests.ts b/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-join-requests.ts new file mode 100644 index 00000000..aebd0cdc --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-join-requests.ts @@ -0,0 +1,151 @@ +import type { Ref } from "vue"; + +import type { DeepnotesApiClient } from "../../api/client"; +import type { components } from "../../api/api-types.generated"; +import type { GroupMembersDetail } from "./group-members-detail"; +import { + buildJoinRequestAcceptBodies, + type InviteCryptoBootstrapJson, +} from "./group-membership-crypto"; +import { readSessionCrypto } from "../auth/session-keyrings"; + +type GroupMemberRole = components["schemas"]["GroupMemberRole"]; + +/** + * Group join request actions: accept, reject. + */ +export function useGroupJoinRequestActions({ + client, + resolvedGroupId, + detail, + actionLoading, + error, + load, +}: { + client: DeepnotesApiClient; + resolvedGroupId: () => string | null; + detail: Ref; + actionLoading: Ref; + error: Ref; + load: () => Promise; +}) { + async function fetchInviteBootstrap(): Promise< + | { ok: true; data: InviteCryptoBootstrapJson } + | { ok: false; error: string } + > { + const id = resolvedGroupId(); + if (id == null) { + return { ok: false, error: "Invalid group." }; + } + const res = await client.GET("/api/groups/{groupId}/invite-crypto-bootstrap", { + params: { path: { groupId: id } }, + }); + if (res.response.status !== 200 || res.data == null) { + const msg = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not load invite crypto material."; + return { ok: false, error: msg }; + } + const d = res.data as InviteCryptoBootstrapJson; + return { ok: true, data: d }; + } + + async function acceptJoinRequestWithCrypto(input: { + requesterUserId: string; + targetRole: GroupMemberRole; + }) { + const id = resolvedGroupId(); + const d = detail.value; + if (id == null || d == null) { + return; + } + const stored = readSessionCrypto(); + if (stored == null) { + error.value = + "Client crypto is not unlocked. Sign in with your account password on this device."; + return; + } + actionLoading.value = true; + error.value = null; + try { + const boot = await fetchInviteBootstrap(); + if (!boot.ok) { + error.value = boot.error; + return; + } + const pkRes = await client.GET("/api/users/{userId}/public-keyring", { + params: { path: { userId: input.requesterUserId } }, + }); + if (pkRes.response.status !== 200 || pkRes.data == null) { + error.value = + pkRes.error && + typeof pkRes.error === "object" && + "message" in pkRes.error + ? String((pkRes.error as { message?: string }).message) + : "Could not load requester public key."; + return; + } + const bodies = await buildJoinRequestAcceptBodies({ + stored, + bootstrap: boot.data, + requesterPublicKeyringB64: pkRes.data.publicKeyring, + groupIsPublic: d.groupIsPublic, + }); + const res = await client.POST( + "/api/groups/{groupId}/join-requests/{userId}/accept", + { + params: { + path: { groupId: id, userId: input.requesterUserId }, + }, + body: { + targetRole: input.targetRole, + encryptedInternalKeyring: bodies.encryptedInternalKeyring, + ...(bodies.encryptedAccessKeyring != null + ? { encryptedAccessKeyring: bodies.encryptedAccessKeyring } + : {}), + }, + }, + ); + if (res.response.status !== 204) { + error.value = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not accept join request."; + return; + } + await load(); + } catch (e) { + error.value = e instanceof Error ? e.message : "Accept request failed."; + } finally { + actionLoading.value = false; + } + } + + async function rejectJoinRequest(requesterUserId: string) { + const id = resolvedGroupId(); + if (id == null) { + return; + } + actionLoading.value = true; + error.value = null; + try { + const res = await client.POST( + "/api/groups/{groupId}/join-requests/{userId}/reject", + { params: { path: { groupId: id, userId: requesterUserId } } }, + ); + if (res.response.status !== 204) { + error.value = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not reject join request."; + return; + } + await load(); + } finally { + actionLoading.value = false; + } + } + + return { acceptJoinRequestWithCrypto, rejectJoinRequest }; +} diff --git a/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-password.ts b/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-password.ts new file mode 100644 index 00000000..8d689fe5 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-password.ts @@ -0,0 +1,202 @@ +import type { Ref } from "vue"; + +import { base64ToBytes } from "@deepnotes/e2ee"; + +import type { DeepnotesApiClient } from "../../api/client"; +import { + buildGroupPasswordEnableRequestBody, + buildGroupPasswordChangeRequestBody, + buildGroupPasswordDisableRequestBody, +} from "./group-password-crypto"; +import { readSessionCrypto } from "../auth/session-keyrings"; + +/** + * Group password management actions: enable, change, disable. + */ +export function useGroupPasswordActions({ + client, + resolvedGroupId, + actionLoading, + error, + load, +}: { + client: DeepnotesApiClient; + resolvedGroupId: () => string | null; + actionLoading: Ref; + error: Ref; + load: () => Promise; +}) { + async function enableGroupPassword(password: string) { + const id = resolvedGroupId(); + if (id == null) { + return; + } + const stored = readSessionCrypto(); + if (stored == null) { + error.value = + "Client crypto is not unlocked. Sign in with your account password on this device."; + return; + } + actionLoading.value = true; + error.value = null; + try { + const ctx = await client.GET("/api/groups/{groupId}/collab-crypto-context", { + params: { path: { groupId: id } }, + }); + if (ctx.response.status !== 200 || ctx.data == null) { + error.value = + ctx.error && typeof ctx.error === "object" && "message" in ctx.error + ? String((ctx.error as { message?: string }).message) + : "Could not load group crypto context."; + return; + } + const body = await buildGroupPasswordEnableRequestBody({ + groupId: id, + password, + groupEncryptedContentKeyring: base64ToBytes(ctx.data.groupEncryptedContentKeyring), + memberEncryptedAccessKeyring: + ctx.data.memberEncryptedAccessKeyring != null + ? base64ToBytes(ctx.data.memberEncryptedAccessKeyring) + : null, + groupAccessKeyring: + ctx.data.groupAccessKeyring != null + ? base64ToBytes(ctx.data.groupAccessKeyring) + : null, + stored, + }); + const res = await client.POST("/api/groups/{groupId}/password", { + params: { path: { groupId: id } }, + body, + }); + if (res.response.status !== 204) { + error.value = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not enable group password."; + return; + } + await load(); + } catch (e) { + error.value = e instanceof Error ? e.message : "Could not enable group password."; + } finally { + actionLoading.value = false; + } + } + + async function changeGroupPassword(currentPassword: string, newPassword: string) { + const id = resolvedGroupId(); + if (id == null) { + return; + } + const stored = readSessionCrypto(); + if (stored == null) { + error.value = + "Client crypto is not unlocked. Sign in with your account password on this device."; + return; + } + actionLoading.value = true; + error.value = null; + try { + const ctx = await client.GET("/api/groups/{groupId}/collab-crypto-context", { + params: { path: { groupId: id } }, + }); + if (ctx.response.status !== 200 || ctx.data == null) { + error.value = + ctx.error && typeof ctx.error === "object" && "message" in ctx.error + ? String((ctx.error as { message?: string }).message) + : "Could not load group crypto context."; + return; + } + const body = await buildGroupPasswordChangeRequestBody({ + groupId: id, + currentPassword, + newPassword, + groupEncryptedContentKeyring: base64ToBytes(ctx.data.groupEncryptedContentKeyring), + memberEncryptedAccessKeyring: + ctx.data.memberEncryptedAccessKeyring != null + ? base64ToBytes(ctx.data.memberEncryptedAccessKeyring) + : null, + groupAccessKeyring: + ctx.data.groupAccessKeyring != null + ? base64ToBytes(ctx.data.groupAccessKeyring) + : null, + stored, + }); + const res = await client.PATCH("/api/groups/{groupId}/password", { + params: { path: { groupId: id } }, + body, + }); + if (res.response.status !== 204) { + error.value = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not change group password."; + return; + } + await load(); + } catch (e) { + error.value = e instanceof Error ? e.message : "Could not change group password."; + } finally { + actionLoading.value = false; + } + } + + async function disableGroupPassword(currentPassword: string) { + const id = resolvedGroupId(); + if (id == null) { + return; + } + const stored = readSessionCrypto(); + if (stored == null) { + error.value = + "Client crypto is not unlocked. Sign in with your account password on this device."; + return; + } + actionLoading.value = true; + error.value = null; + try { + const ctx = await client.GET("/api/groups/{groupId}/collab-crypto-context", { + params: { path: { groupId: id } }, + }); + if (ctx.response.status !== 200 || ctx.data == null) { + error.value = + ctx.error && typeof ctx.error === "object" && "message" in ctx.error + ? String((ctx.error as { message?: string }).message) + : "Could not load group crypto context."; + return; + } + const body = await buildGroupPasswordDisableRequestBody({ + groupId: id, + currentPassword, + groupEncryptedContentKeyring: base64ToBytes(ctx.data.groupEncryptedContentKeyring), + memberEncryptedAccessKeyring: + ctx.data.memberEncryptedAccessKeyring != null + ? base64ToBytes(ctx.data.memberEncryptedAccessKeyring) + : null, + groupAccessKeyring: + ctx.data.groupAccessKeyring != null + ? base64ToBytes(ctx.data.groupAccessKeyring) + : null, + stored, + }); + const res = await client.DELETE("/api/groups/{groupId}/password", { + params: { path: { groupId: id } }, + body, + }); + if (res.response.status !== 204) { + error.value = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not disable group password."; + return; + } + await load(); + } catch (e) { + error.value = e instanceof Error ? e.message : "Could not disable group password."; + } finally { + actionLoading.value = false; + } + } + + return { enableGroupPassword, changeGroupPassword, disableGroupPassword }; +} diff --git a/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-privacy.ts b/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-privacy.ts new file mode 100644 index 00000000..d97a883f --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail-privacy.ts @@ -0,0 +1,189 @@ +import type { Ref } from "vue"; + +import type { DeepnotesApiClient } from "../../api/client"; +import type { GroupMembersDetail } from "./group-members-detail"; +import { + buildMakePublicAccessKeyringB64, + type InviteCryptoBootstrapJson, +} from "./group-membership-crypto"; +import { + buildGroupPrivacyMakePrivateRequest, + type GroupPrivacyMakePrivateBootstrapJson, +} from "./group-make-private-crypto"; +import { readSessionCrypto } from "../auth/session-keyrings"; + +/** + * Group privacy actions: make public/private, allow join requests. + */ +export function useGroupPrivacyActions({ + client, + resolvedGroupId, + detail, + actionLoading, + error, + load, +}: { + client: DeepnotesApiClient; + resolvedGroupId: () => string | null; + detail: Ref; + actionLoading: Ref; + error: Ref; + load: () => Promise; +}) { + async function fetchInviteBootstrap(): Promise< + | { ok: true; data: InviteCryptoBootstrapJson } + | { ok: false; error: string } + > { + const id = resolvedGroupId(); + if (id == null) { + return { ok: false, error: "Invalid group." }; + } + const res = await client.GET("/api/groups/{groupId}/invite-crypto-bootstrap", { + params: { path: { groupId: id } }, + }); + if (res.response.status !== 200 || res.data == null) { + const msg = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not load invite crypto material."; + return { ok: false, error: msg }; + } + const d = res.data as InviteCryptoBootstrapJson; + return { ok: true, data: d }; + } + + async function makeGroupPublic() { + const id = resolvedGroupId(); + const d = detail.value; + if (id == null || d == null) { + return; + } + if (d.groupIsPublic) { + error.value = "Group is already public."; + return; + } + const stored = readSessionCrypto(); + if (stored == null) { + error.value = + "Client crypto is not unlocked. Sign in with your account password on this device."; + return; + } + actionLoading.value = true; + error.value = null; + try { + const boot = await fetchInviteBootstrap(); + if (!boot.ok) { + error.value = boot.error; + return; + } + const accessKeyring = await buildMakePublicAccessKeyringB64({ + stored, + bootstrap: boot.data, + }); + const res = await client.POST("/api/groups/{groupId}/privacy/public", { + params: { path: { groupId: id } }, + body: { accessKeyring }, + }); + if (res.response.status !== 204) { + error.value = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not make group public."; + return; + } + await load(); + } catch (e) { + error.value = e instanceof Error ? e.message : "Could not make group public."; + } finally { + actionLoading.value = false; + } + } + + async function makeGroupPrivate() { + const id = resolvedGroupId(); + const d = detail.value; + if (id == null || d == null) { + return; + } + if (!d.groupIsPublic) { + error.value = "Group is already private."; + return; + } + const stored = readSessionCrypto(); + if (stored == null) { + error.value = + "Client crypto is not unlocked. Sign in with your account password on this device."; + return; + } + actionLoading.value = true; + error.value = null; + try { + const bootRes = await client.GET( + "/api/groups/{groupId}/privacy/make-private-bootstrap", + { params: { path: { groupId: id } } }, + ); + if (bootRes.response.status !== 200 || bootRes.data == null) { + error.value = + bootRes.error && + typeof bootRes.error === "object" && + "message" in bootRes.error + ? String((bootRes.error as { message?: string }).message) + : "Could not load make-private bootstrap."; + return; + } + const body = await buildGroupPrivacyMakePrivateRequest({ + groupId: id, + groupIsPublic: false, + stored, + bootstrap: bootRes.data as GroupPrivacyMakePrivateBootstrapJson, + }); + const res = await client.POST("/api/groups/{groupId}/privacy/private", { + params: { path: { groupId: id } }, + body, + }); + if (res.response.status !== 204) { + error.value = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not make group private."; + return; + } + await load(); + } catch (e) { + error.value = + e instanceof Error ? e.message : "Could not make group private."; + } finally { + actionLoading.value = false; + } + } + + async function setJoinRequestsAllowed(allowed: boolean) { + const id = resolvedGroupId(); + if (id == null) { + return; + } + actionLoading.value = true; + error.value = null; + try { + const res = await client.PATCH( + "/api/groups/{groupId}/privacy/join-requests", + { + params: { path: { groupId: id } }, + body: { areJoinRequestsAllowed: allowed }, + }, + ); + if (res.response.status !== 204) { + error.value = + res.error && typeof res.error === "object" && "message" in res.error + ? String((res.error as { message?: string }).message) + : "Could not update join request policy."; + return; + } + await load(); + } finally { + actionLoading.value = false; + } + } + + return { makeGroupPublic, makeGroupPrivate, setJoinRequestsAllowed }; +} diff --git a/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail.ts b/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail.ts new file mode 100644 index 00000000..ddcde1b7 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/useGroupMembersDetail.ts @@ -0,0 +1,116 @@ +import { ref, type Ref } from "vue"; + +/** Route `params.groupId` may be undefined until matched. */ +export type GroupIdParamRef = Ref; + +import { useSession } from "../auth/useSession"; +import { + fetchGroupMembersDetail, + type GroupMembersDetail, +} from "./group-members-detail"; +import { useGroupMemberActions } from "./useGroupMembersDetail-actions"; +import { useGroupInvitationActions } from "./useGroupMembersDetail-invitations"; +import { useGroupJoinRequestActions } from "./useGroupMembersDetail-join-requests"; +import { useGroupPrivacyActions } from "./useGroupMembersDetail-privacy"; +import { useGroupDeletionActions } from "./useGroupMembersDetail-deletion"; +import { useGroupPasswordActions } from "./useGroupMembersDetail-password"; + +export function useGroupMembersDetail(groupId: GroupIdParamRef) { + const loading: Ref = ref(false); + const actionLoading: Ref = ref(false); + const error: Ref = ref(null); + const detail: Ref = ref(null); + + const { client, isAuthenticated, user } = useSession(); + + function resolvedGroupId(): string | null { + const g = groupId.value; + const id = Array.isArray(g) ? g[0] : g; + return id && /^[A-Za-z0-9_-]{21}$/.test(id) ? id : null; + } + + async function load() { + const id = resolvedGroupId(); + if (!isAuthenticated.value || id == null) { + detail.value = null; + return; + } + loading.value = true; + error.value = null; + try { + const out = await fetchGroupMembersDetail({ client, groupId: id }); + detail.value = out.data; + error.value = out.error; + } finally { + loading.value = false; + } + } + + const memberActions = useGroupMemberActions({ + client, + resolvedGroupId, + detail, + actionLoading, + error, + load, + user, + }); + + const invitationActions = useGroupInvitationActions({ + client, + resolvedGroupId, + detail, + actionLoading, + error, + load, + user, + }); + + const joinRequestActions = useGroupJoinRequestActions({ + client, + resolvedGroupId, + detail, + actionLoading, + error, + load, + }); + + const privacyActions = useGroupPrivacyActions({ + client, + resolvedGroupId, + detail, + actionLoading, + error, + load, + }); + + const deletionActions = useGroupDeletionActions({ + client, + resolvedGroupId, + detail, + actionLoading, + error, + }); + + const passwordActions = useGroupPasswordActions({ + client, + resolvedGroupId, + actionLoading, + error, + load, + }); + + return { + loading, + actionLoading, + error, + detail, + load, + ...memberActions, + ...invitationActions, + ...joinRequestActions, + ...privacyActions, + ...deletionActions, + ...passwordActions, + }; +} diff --git a/new-deepnotes/apps/web/src/features/groups/useGroupsOverview.ts b/new-deepnotes/apps/web/src/features/groups/useGroupsOverview.ts new file mode 100644 index 00000000..ea14a194 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/groups/useGroupsOverview.ts @@ -0,0 +1,38 @@ +import { ref, type Ref } from "vue"; + +import { useSession } from "../auth/useSession"; +import { fetchGroupsOverview, type GroupOverviewRow } from "./groups-overview"; + +export function useGroupsOverview() { + const loading: Ref = ref(false); + const error: Ref = ref(null); + const rows: Ref = ref([]); + + const { client, user, isAuthenticated } = useSession(); + + async function load() { + if (!isAuthenticated.value || user.value == null) { + rows.value = []; + return; + } + loading.value = true; + error.value = null; + try { + const out = await fetchGroupsOverview({ + client, + personalGroupId: user.value.personalGroupId, + }); + rows.value = out.rows; + error.value = out.error; + } finally { + loading.value = false; + } + } + + return { + loading, + error, + rows, + load, + }; +} diff --git a/new-deepnotes/apps/web/src/features/home/HomeView.vue b/new-deepnotes/apps/web/src/features/home/HomeView.vue new file mode 100644 index 00000000..9233bb26 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/home/HomeView.vue @@ -0,0 +1,301 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/home/home-routes.ts b/new-deepnotes/apps/web/src/features/home/home-routes.ts new file mode 100644 index 00000000..c09bb704 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/home/home-routes.ts @@ -0,0 +1,9 @@ +import type { RouteRecordRaw } from "vue-router"; + +export const homeRoutes: RouteRecordRaw[] = [ + { + path: "/", + name: "home", + redirect: "/pages", + }, +]; diff --git a/new-deepnotes/apps/web/src/features/notifications/NotificationsPopover.vue b/new-deepnotes/apps/web/src/features/notifications/NotificationsPopover.vue new file mode 100644 index 00000000..25ea2ec9 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/notifications/NotificationsPopover.vue @@ -0,0 +1,143 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/notifications/NotificationsView.vue b/new-deepnotes/apps/web/src/features/notifications/NotificationsView.vue new file mode 100644 index 00000000..87097bf7 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/notifications/NotificationsView.vue @@ -0,0 +1,150 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/notifications/decrypt-notification-body.test.ts b/new-deepnotes/apps/web/src/features/notifications/decrypt-notification-body.test.ts new file mode 100644 index 00000000..0dc938c6 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/notifications/decrypt-notification-body.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; + +import { formatNotificationPayload } from "./decrypt-notification-body"; + +describe("formatNotificationPayload", () => { + it("formats objects as JSON", () => { + expect(formatNotificationPayload({ hello: "world" })).toContain('"hello"'); + }); + + it("returns strings as-is", () => { + expect(formatNotificationPayload("plain")).toBe("plain"); + }); +}); diff --git a/new-deepnotes/apps/web/src/features/notifications/decrypt-notification-body.ts b/new-deepnotes/apps/web/src/features/notifications/decrypt-notification-body.ts new file mode 100644 index 00000000..594784d4 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/notifications/decrypt-notification-body.ts @@ -0,0 +1,69 @@ +import { unpack } from "msgpackr"; + +import { + base64ToBytes, + createPrivateKeyring, + ensureSodiumReady, + wrapSymmetricKey, +} from "@deepnotes/e2ee"; + +import { readSessionCrypto } from "../auth/crypto-storage"; + +/** + * Mirrors legacy realtime `USER_NOTIFICATION`: unwrap session private keyring, + * box-open symmetric key ciphertext, symmetric-decrypt notification body with + * `UserNotificationContent` AEAD scope, then unpack msgpack. + */ +export async function tryDecryptNotificationBody(input: { + encryptedSymmetricKey: string; + encryptedContent: string; +}): Promise { + const stored = readSessionCrypto(); + if (stored == null) { + return null; + } + await ensureSodiumReady(); + const sessionKey = wrapSymmetricKey(base64ToBytes(stored.sessionKeyB64)); + + try { + const privateKeyring = createPrivateKeyring( + base64ToBytes(stored.encryptedPrivateKeyringB64), + ).unwrapSymmetric(sessionKey, { + associatedData: { + context: "SessionUserPrivateKeyring", + userId: stored.userId, + }, + }); + + const symmetricKeyMat = privateKeyring.decrypt( + base64ToBytes(input.encryptedSymmetricKey), + { padding: true }, + ); + + const symmetricKey = wrapSymmetricKey(symmetricKeyMat); + + const plaintext = symmetricKey.decrypt( + base64ToBytes(input.encryptedContent), + { + padding: true, + associatedData: { context: "UserNotificationContent" }, + }, + ); + + return unpack(plaintext); + } catch { + return null; + } +} + +/** Present msgpack payloads readably in the UI. */ +export function formatNotificationPayload(value: unknown): string { + if (typeof value === "string") { + return value; + } + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} diff --git a/new-deepnotes/apps/web/src/features/notifications/notifications-list.test.ts b/new-deepnotes/apps/web/src/features/notifications/notifications-list.test.ts new file mode 100644 index 00000000..32ef470e --- /dev/null +++ b/new-deepnotes/apps/web/src/features/notifications/notifications-list.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it, vi } from "vitest"; + +import type { DeepnotesApiClient } from "../../api/client"; +import { + fetchNotificationsPage, + isNotificationUnread, + markAllNotificationsRead, +} from "./notifications-list"; + +describe("isNotificationUnread", () => { + it("treats all as unread when the user has no read cursor", () => { + expect(isNotificationUnread(42, null)).toBe(true); + }); + + it("compares id to the stored read cursor", () => { + expect(isNotificationUnread(5, 4)).toBe(true); + expect(isNotificationUnread(4, 4)).toBe(false); + expect(isNotificationUnread(3, 4)).toBe(false); + }); +}); + +describe("fetchNotificationsPage", () => { + it("maps items and first-page read cursor", async () => { + const client = { + GET: vi.fn().mockResolvedValue({ + response: { status: 200 }, + data: { + items: [ + { + id: 10, + type: "group-invite", + encryptedSymmetricKey: "YQ==", + encryptedContent: "Yg==", + dateTime: "2026-01-01T12:00:00.000Z", + }, + ], + hasMore: false, + lastNotificationRead: 4, + }, + }), + }; + const out = await fetchNotificationsPage({ + client: client as unknown as DeepnotesApiClient, + }); + expect(out.error).toBeNull(); + expect(out.hasMore).toBe(false); + expect(out.lastNotificationRead).toBe(4); + expect(out.rows[0]).toMatchObject({ + id: 10, + type: "group-invite", + unread: true, + }); + }); + + it("uses readCursorForUnread when loading older pages", async () => { + const client = { + GET: vi.fn().mockResolvedValue({ + response: { status: 200 }, + data: { + items: [ + { + id: 2, + type: "old", + encryptedSymmetricKey: "YQ==", + encryptedContent: "Yg==", + dateTime: "2025-01-01T12:00:00.000Z", + }, + ], + hasMore: false, + }, + }), + }; + const out = await fetchNotificationsPage({ + client: client as unknown as DeepnotesApiClient, + lastNotificationId: 10, + readCursorForUnread: 5, + }); + expect(out.rows[0]?.unread).toBe(false); + }); + + it("returns an error on failed GET", async () => { + const client = { + GET: vi.fn().mockResolvedValue({ + response: { status: 401 }, + error: { message: "No session" }, + data: undefined, + }), + }; + const out = await fetchNotificationsPage({ + client: client as unknown as DeepnotesApiClient, + }); + expect(out.rows).toEqual([]); + expect(out.error).toBe("No session"); + }); +}); + +describe("markAllNotificationsRead", () => { + it("returns ok on 204", async () => { + const client = { + POST: vi.fn().mockResolvedValue({ + response: { status: 204 }, + }), + }; + const out = await markAllNotificationsRead({ + client: client as unknown as DeepnotesApiClient, + }); + expect(out).toEqual({ ok: true, error: null }); + }); +}); diff --git a/new-deepnotes/apps/web/src/features/notifications/notifications-list.ts b/new-deepnotes/apps/web/src/features/notifications/notifications-list.ts new file mode 100644 index 00000000..dc06be99 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/notifications/notifications-list.ts @@ -0,0 +1,134 @@ +import type { DeepnotesApiClient } from "../../api/client"; + +export type NotificationRow = { + id: number; + type: string; + dateTime: string; + /** True when this row is newer than the serverโ€™s read cursor (or cursor unset). */ + unread: boolean; + encryptedSymmetricKey: string; + encryptedContent: string; + /** Filled client-side after `tryDecryptNotificationBody` succeeds. */ + decryptedText?: string | null; +}; + +import { + formatNotificationPayload, + tryDecryptNotificationBody, +} from "./decrypt-notification-body"; + +/** + * Fill `decryptedText` where session keyrings can unlock the ciphertext. + */ +export async function attachDecryptedNotificationText( + rows: NotificationRow[], +): Promise { + const out = await Promise.all( + rows.map(async (r) => { + const dec = await tryDecryptNotificationBody({ + encryptedSymmetricKey: r.encryptedSymmetricKey, + encryptedContent: r.encryptedContent, + }); + return { + ...r, + decryptedText: + dec === null ? null : formatNotificationPayload(dec), + }; + }), + ); + return out; +} + +/** + * `GET /api/users/me/notifications` with optional older-than pagination. + * The API includes `lastNotificationRead` on the first page only; for `load more`, + * pass the same cursor you stored from the first response so unread badges stay + * correct. + */ +export async function fetchNotificationsPage(input: { + client: DeepnotesApiClient; + lastNotificationId?: number; + /** Required when `lastNotificationId` is set โ€” the read cursor from the first page. */ + readCursorForUnread?: number | null; +}): Promise<{ + rows: NotificationRow[]; + hasMore: boolean; + lastNotificationRead: number | null | undefined; + error: string | null; +}> { + const { client, lastNotificationId, readCursorForUnread } = input; + const res = await client.GET("/api/users/me/notifications", { + params: { + query: + lastNotificationId != null ? { lastNotificationId } : {}, + }, + }); + + if (res.response.status !== 200 || !res.data) { + if (res.error && typeof res.error === "object" && "message" in res.error) { + return { + rows: [], + hasMore: false, + lastNotificationRead: undefined, + error: String((res.error as { message?: string }).message), + }; + } + return { + rows: [], + hasMore: false, + lastNotificationRead: undefined, + error: "Could not load notifications.", + }; + } + + const { items, hasMore, lastNotificationRead } = res.data; + const readCursor = + lastNotificationId == null + ? (lastNotificationRead ?? null) + : (readCursorForUnread ?? null); + + const rows: NotificationRow[] = items.map((it) => ({ + id: it.id, + type: it.type, + dateTime: it.dateTime, + unread: isNotificationUnread(it.id, readCursor), + encryptedSymmetricKey: it.encryptedSymmetricKey, + encryptedContent: it.encryptedContent, + decryptedText: undefined, + })); + + return { + rows, + hasMore: Boolean(hasMore), + lastNotificationRead: + lastNotificationId == null ? (lastNotificationRead ?? null) : undefined, + error: null, + }; +} + +/** Server stores the newest notification id the user has acknowledged. */ +export function isNotificationUnread( + notificationId: number, + lastNotificationRead: number | null, +): boolean { + if (lastNotificationRead == null) { + return true; + } + return notificationId > lastNotificationRead; +} + +export async function markAllNotificationsRead(input: { + client: DeepnotesApiClient; +}): Promise<{ ok: boolean; error: string | null }> { + const res = await input.client.POST("/api/users/me/notifications/read", {}); + if (res.response.status === 204) { + return { ok: true, error: null }; + } + if (res.error && typeof res.error === "object" && "message" in res.error) { + return { + ok: false, + error: String((res.error as { message?: string }).message), + }; + } + return { ok: false, error: "Could not update read state." }; +} diff --git a/new-deepnotes/apps/web/src/features/notifications/notifications-routes.ts b/new-deepnotes/apps/web/src/features/notifications/notifications-routes.ts new file mode 100644 index 00000000..081d13ab --- /dev/null +++ b/new-deepnotes/apps/web/src/features/notifications/notifications-routes.ts @@ -0,0 +1,9 @@ +import type { RouteRecordRaw } from "vue-router"; + +export const notificationsRoutes: RouteRecordRaw[] = [ + { + path: "/notifications", + name: "notifications", + component: () => import("./NotificationsView.vue"), + }, +]; diff --git a/new-deepnotes/apps/web/src/features/notifications/useNotificationBadge.ts b/new-deepnotes/apps/web/src/features/notifications/useNotificationBadge.ts new file mode 100644 index 00000000..35c386d6 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/notifications/useNotificationBadge.ts @@ -0,0 +1,28 @@ +import { ref, type Ref } from "vue"; + +/** + * Global unread notification count for the toolbar badge. + * Updated when notifications are loaded or when new realtime notifications arrive. + */ +export const unreadNotificationCount: Ref = ref(0); + +/** + * Increment the unread count (called when a realtime notification arrives). + */ +export function incrementUnreadCount() { + unreadNotificationCount.value += 1; +} + +/** + * Set the unread count from the notifications list (called when user visits /notifications). + */ +export function setUnreadCount(count: number) { + unreadNotificationCount.value = count; +} + +/** + * Reset the unread count to 0 (called when user marks all as read). + */ +export function resetUnreadCount() { + unreadNotificationCount.value = 0; +} diff --git a/new-deepnotes/apps/web/src/features/notifications/useNotifications.ts b/new-deepnotes/apps/web/src/features/notifications/useNotifications.ts new file mode 100644 index 00000000..30d85479 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/notifications/useNotifications.ts @@ -0,0 +1,112 @@ +import { ref, type Ref } from "vue"; + +import { useSession } from "../auth/useSession"; +import { + attachDecryptedNotificationText, + fetchNotificationsPage, + markAllNotificationsRead, + type NotificationRow, +} from "./notifications-list"; +import { resetUnreadCount, setUnreadCount } from "./useNotificationBadge"; + +export function useNotifications() { + const loading: Ref = ref(false); + const error: Ref = ref(null); + const rows: Ref = ref([]); + const hasMore: Ref = ref(false); + const lastReadCursor: Ref = ref(null); + const markingRead: Ref = ref(false); + + const { client, isAuthenticated } = useSession(); + + async function loadFirst() { + if (!isAuthenticated.value) { + rows.value = []; + hasMore.value = false; + lastReadCursor.value = null; + setUnreadCount(0); + return; + } + loading.value = true; + error.value = null; + try { + const out = await fetchNotificationsPage({ client }); + rows.value = await attachDecryptedNotificationText(out.rows); + hasMore.value = out.hasMore; + lastReadCursor.value = + out.lastNotificationRead === undefined + ? null + : (out.lastNotificationRead ?? null); + error.value = out.error; + // Update badge count from loaded notifications + const unreadCount = rows.value.filter((r) => r.unread).length; + setUnreadCount(unreadCount); + } finally { + loading.value = false; + } + } + + async function loadMore() { + if (!isAuthenticated.value || rows.value.length === 0) { + return; + } + const oldest = rows.value[rows.value.length - 1]; + if (oldest == null) { + return; + } + loading.value = true; + error.value = null; + try { + const out = await fetchNotificationsPage({ + client, + lastNotificationId: oldest.id, + readCursorForUnread: lastReadCursor.value, + }); + if (out.error) { + error.value = out.error; + return; + } + const more = await attachDecryptedNotificationText(out.rows); + rows.value = [...rows.value, ...more]; + hasMore.value = out.hasMore; + } finally { + loading.value = false; + } + } + + async function markRead() { + if (!isAuthenticated.value) { + return; + } + markingRead.value = true; + error.value = null; + try { + const out = await markAllNotificationsRead({ client }); + if (!out.ok) { + error.value = out.error; + return; + } + const maxId = + rows.value.length > 0 + ? Math.max(...rows.value.map((r) => r.id)) + : null; + lastReadCursor.value = maxId; + rows.value = rows.value.map((r) => ({ ...r, unread: false })); + resetUnreadCount(); + } finally { + markingRead.value = false; + } + } + + return { + loading, + error, + rows, + hasMore, + lastReadCursor, + markingRead, + loadFirst, + loadMore, + markRead, + }; +} diff --git a/new-deepnotes/apps/web/src/features/pages/FavoritePagesCard.test.ts b/new-deepnotes/apps/web/src/features/pages/FavoritePagesCard.test.ts new file mode 100644 index 00000000..0821c9a0 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/pages/FavoritePagesCard.test.ts @@ -0,0 +1,82 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { mount } from "@vue/test-utils"; +import { defineComponent, h } from "vue"; + +vi.mock("vue-router", () => ({ + RouterLink: defineComponent({ + props: ["to"], + setup(props, { slots }) { + return () => h("a", { "data-testid": "router-link", "data-to": props.to }, slots); + }, + }), +})); + +import FavoritePagesCard from "./FavoritePagesCard.vue"; + +describe("FavoritePagesCard", () => { + let wrapper: ReturnType | undefined; + + afterEach(() => { + wrapper?.unmount(); + wrapper = undefined; + }); + + it("renders empty state when no favorite pages", () => { + wrapper = mount(FavoritePagesCard, { + props: { favoritePageIds: [], currentPageId: "p1", pageLabels: {} }, + }); + expect(wrapper.text()).toContain("No favorite pages"); + }); + + it("renders favorite page links with labels", () => { + wrapper = mount(FavoritePagesCard, { + props: { + favoritePageIds: ["p1", "p2"], + currentPageId: "p1", + pageLabels: { p1: "Alpha", p2: "Beta" }, + }, + }); + + const links = wrapper.findAll('[data-testid="router-link"]'); + expect(links).toHaveLength(2); + expect(links[0]!.text()).toBe("Alpha"); + expect(links[1]!.text()).toBe("Beta"); + }); + + it("highlights current page link", () => { + wrapper = mount(FavoritePagesCard, { + props: { + favoritePageIds: ["p1", "p2"], + currentPageId: "p1", + pageLabels: { p1: "Alpha", p2: "Beta" }, + }, + }); + + const links = wrapper.findAll('[data-testid="router-link"]'); + expect(links[0]!.classes()).toContain("bg-accent"); + expect(links[1]!.classes()).not.toContain("bg-accent"); + }); + + it("emits clear when clear button clicked", async () => { + wrapper = mount(FavoritePagesCard, { + props: { + favoritePageIds: ["p1"], + currentPageId: "p1", + pageLabels: {}, + }, + }); + + const button = wrapper.find("button"); + await button.trigger("click"); + expect(wrapper.emitted("clear")).toHaveLength(1); + }); + + it("disables clear button when no favorite pages", () => { + wrapper = mount(FavoritePagesCard, { + props: { favoritePageIds: [], currentPageId: "p1", pageLabels: {} }, + }); + + const button = wrapper.find("button"); + expect(button.attributes("disabled")).toBeDefined(); + }); +}); diff --git a/new-deepnotes/apps/web/src/features/pages/FavoritePagesCard.vue b/new-deepnotes/apps/web/src/features/pages/FavoritePagesCard.vue new file mode 100644 index 00000000..61883fd5 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/pages/FavoritePagesCard.vue @@ -0,0 +1,56 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/pages/InlineMathNodeView.vue b/new-deepnotes/apps/web/src/features/pages/InlineMathNodeView.vue new file mode 100644 index 00000000..db911c8d --- /dev/null +++ b/new-deepnotes/apps/web/src/features/pages/InlineMathNodeView.vue @@ -0,0 +1,95 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/pages/MathBlockNodeView.vue b/new-deepnotes/apps/web/src/features/pages/MathBlockNodeView.vue new file mode 100644 index 00000000..58fcf9a4 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/pages/MathBlockNodeView.vue @@ -0,0 +1,92 @@ + + +