diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..ff25b3e --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,33 @@ +name: Quality + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + quality: + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.3.12 + - run: bun install --frozen-lockfile + - run: bun run lint + - run: bun run typecheck + - run: bun run test + - run: bun run build + # Advisory only: bun audit reports dev-tooling CVEs with no production fix yet. + - run: bun audit || true \ No newline at end of file diff --git a/.gitignore b/.gitignore index bd1618c..fe14774 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,6 @@ count.txt __unconfig* todos.json local.db -react-doctor-report.json +react-doctor-report.json +plans/ +mcps/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f4376c4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,77 @@ +# CLAUDE.md + +Guidance for AI agents and new contributors working in this repo. For product +intent see `PRD.md`, for roadmap/status see `phases.md`, and for the latest +hardening audit see `docs/audits/production-readiness-audit.md`. + +## What this is + +Clientra — a deliberately lightweight client/project management app for +freelancers and small agencies. An **admin** (the operator) manages clients and +projects; external **clients** get a read-mostly portal. + +**Non-goals (do not build these):** enterprise CRM workflows, marketing/sales +pipelines, complex automation, and billing/invoicing/payments. Keep changes +lightweight and in line with `PRD.md`. + +## Stack + +TanStack Start (React 19) · Drizzle ORM + Turso (libSQL/SQLite) · Better Auth · +Zod · Bun · Vitest · Biome + Ultracite · Tailwind v4 + shadcn/ui. + +## Commands + +- `bun dev` — dev server (port 3000) +- `bun run test` — Vitest +- `bun run lint` — Biome (via Ultracite) +- `bun run typecheck` — `tsc --noEmit` +- `bun run build` — Vite build +- `bun run db:studio` — Drizzle Studio +- `bun run db:sync` — push schema + seed (local/dev) + +Before opening a PR, all of `lint`, `typecheck`, `test`, and `build` must pass. + +## Layout + +- `src/routes/**` — pages; `src/routes/api/**` — server route handlers. +- `src/server/http/route-utils.ts` — request guards: `requireSessionRequest`, + `requireMutationSessionRequest`, `requireAdminMutationRequest`, + `requireSameOrigin`, plus JSON/error helpers. Use these; don't hand-roll auth. +- `src/db/records.ts` — the data-access layer (large; being split by domain). +- `src/db/schema.ts` — Drizzle schema; migrations live in `drizzle/` and are + generated with `drizzle-kit generate`. +- `src/auth/**` — Better Auth config (`better-auth.ts`), roles (`roles.ts`), + session helpers (`session.server.ts`). +- `src/lib/api.ts` — client-side fetch + React Query hooks (large; being split). +- `src/__tests__/**` — tests. DB-backed tests apply `drizzle/*.sql` to a temp + `file:` libSQL database — see `records-collaboration.test.ts` for the pattern. + +## Conventions + +- **Auth on every mutating route**: same-origin + session (+ role/ownership) + checks via the guards in `route-utils.ts`. Client-reachable routes that touch a + project/file/note must call `canAccessProject` (in `records.ts`). +- **Validate input with Zod** via `parseJsonBody(request, schema)` before use. +- **Roles**: `ROLES.ADMIN` / `ROLES.CLIENT` from `src/auth/roles.ts` — never + hardcode the strings. The `role` field is server-controlled (not settable at + signup). +- **Error handling**: return the typed helpers from `route-utils.ts` + (`unauthorizedError`, `forbiddenError`, `notFoundError`, …), not ad-hoc responses. +- Match the surrounding file's style; run `bun run fix` (Ultracite) to auto-format. + +## Known debt / gotchas + +- `src/db/records.ts` and `src/lib/api.ts` are oversized and being split by domain + (see `plans/`). Prefer adding to the right domain seam over growing them. +- `src/components/evilcharts/**` is vendored chart code and is excluded from + linting — don't hand-fix lint there. +- Seeding (`seedIfEmpty`) is for local/dev only — enforced by a non-local DB + guard; it must not run from request handlers or against production Turso. +- Multi-tenancy (Better Auth `organization` plugin + `organizationId` columns) is + partially wired — confirm scoping before relying on it. + +## Where to look first + +- A bug in data access → `src/db/records.ts` + the matching `src/__tests__/` file. +- An auth/access question → `src/auth/` + `src/server/http/route-utils.ts`. +- A page/data-flow question → the route in `src/routes/` + `src/lib/api.ts`. \ No newline at end of file diff --git a/biome.jsonc b/biome.jsonc index e2b8420..f2637af 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -4,5 +4,11 @@ "ultracite/biome/core", "ultracite/biome/react", "ultracite/biome/vitest" + ], + "overrides": [ + { + "includes": ["src/components/evilcharts/**"], + "linter": { "enabled": false } + } ] } diff --git a/bun.lock b/bun.lock index 8533392..0bfc064 100644 --- a/bun.lock +++ b/bun.lock @@ -59,8 +59,6 @@ "packages": { "@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="], - "@antfu/ni": ["@antfu/ni@25.0.0", "", { "dependencies": { "ansis": "^4.0.0", "fzf": "^0.5.2", "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" }, "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs", "nup": "bin/nup.mjs" } }, "sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA=="], - "@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.2", "", { "dependencies": { "@csstools/css-calc": "^3.0.0", "@csstools/css-color-parser": "^4.0.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.2.5" } }, "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg=="], "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.8.1", "", { "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" } }, "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ=="], @@ -125,27 +123,27 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@base-ui/react": ["@base-ui/react@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@base-ui/utils": "0.2.6", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-FwpKqZbPz14AITp1CVgf4AjhKPe1OeeVKSBMdgD10zbFlj3QSWelmtCMLi2+/PFZZcIm3l87G7rwtCZJwHyXWA=="], + "@base-ui/react": ["@base-ui/react@1.6.0", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.3.1", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-/jzjTWJYXhRFO45Bev9lc3cHbmjzCMpUqbMZ2AgKy/z25mY9B6shGSNcXcjQar9n5doM0KYW1W8fcFv2jZBuMw=="], - "@base-ui/utils": ["@base-ui/utils@0.2.6", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-yQ+qeuqohwhsNpoYDqqXaLllYAkPCP4vYdDrVo8FQXaAPfHWm1pG/Vm+jmGTA5JFS0BAIjookyapuJFY8F9PIw=="], + "@base-ui/utils": ["@base-ui/utils@0.3.1", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg=="], - "@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], + "@better-auth/core": ["@better-auth/core@1.6.20", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.6", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-y73I1xNXuNYiHBFduWGRcJ2ro2rNuVDEYkgVMJtIaRXtbosdXHs9gfyQrHecgeHMHKx1SYSBT/CExak0vVMTng=="], - "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-HAi9xAP40oDt48QZeYBFTcmg3vt1Jik90GwoRIfangd7VGbxesIIDBJSnvwMbZ52GBIc6+V4FRw9lasNiNrPfw=="], + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.20", "", { "peerDependencies": { "@better-auth/core": "^1.6.20", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-hJHfCdAiZrC7EmZAt3NAiGgcNo9Y5Qz3PLL+a9rODXaAJGCMvzUJniqef9wHuJBwU0SWW+2f4wXe8xQmaC/IKQ=="], - "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "kysely": "^0.27.0 || ^0.28.0" } }, "sha512-LmHffIVnqbfsxcxckMOoE8MwibWrbVFch+kwPKJ5OFDFv6lin75ufN7ZZ7twH0IMPLT/FcgzaRjP8jRrXRef9g=="], + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.20", "", { "peerDependencies": { "@better-auth/core": "^1.6.20", "@better-auth/utils": "0.4.2", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-Uvpmgbx5y8JqXroVanNzDdKzOl3HojoTz+/X6MR6zOUr25IzlYz660mjnu0rxKiIF55kD3CroqFsDzjNUw7ERw=="], - "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0" } }, "sha512-4X0j1/2L+nsgmObjmy9xEGUFWUv38Qjthp558fwS3DAp6ueWWyCaxaD6VJZ7m5qPNMrsBStO5WGP8CmJTEWm7g=="], + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.20", "", { "peerDependencies": { "@better-auth/core": "^1.6.20", "@better-auth/utils": "0.4.2" } }, "sha512-J5Ni0LlFijbzXlwu2rFHaD8zEFocmajyzWkRnHsq8LhV/Dk4iWQwwnqzLrPoDQEj8roECAUF03hrIeMzqWRqJQ=="], - "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "mongodb": "^6.0.0 || ^7.0.0" } }, "sha512-P1J9ljL5X5k740I8Rx1esPWNgWYPdJR5hf2CY7BwDSrQFPUHuzeCg0YhtEEP55niNateTXhBqGAcy0fVOeamZg=="], + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.20", "", { "peerDependencies": { "@better-auth/core": "^1.6.20", "@better-auth/utils": "0.4.2", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-ClDBJf6h4g85WJswxwQwxLaiyRU67Gmz/uaIf19tY1gqlLJDykSGjmqRNSBMG5rWABNzcNqbO4KG31rYUldbIw=="], - "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-CliDd78CXHzzwQIXhCdwGr5Ml53i6JdCHWV7PYwTIJz9EAm6qb2RVBdpP3nqEfNjINGM22A6gfleCgCdZkTIZg=="], + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.20", "", { "peerDependencies": { "@better-auth/core": "^1.6.20", "@better-auth/utils": "0.4.2", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-WhYdhSGuVSfu1peCSf2snmmVzfWjRaEvbSrsNCusiwGE9l94HlES4mjSPM48fed24hL7yg4j1dYK/yjEt87FpQ=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.5.5", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.5" } }, "sha512-1+lklxArn4IMHuU503RcPdXrSG2tlXt4jnGG3omolmspQ7tktg/Y9XO/yAkYDurtvMn1xJ8X1Ov01Ji/r5s9BQ=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.6.20", "", { "peerDependencies": { "@better-auth/core": "^1.6.20", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1" } }, "sha512-3BhbY3naQDERvdJvJ7fGszVY6rpsVfc6c9uyBVZlC1coVEF/rkM0rIcjtMVI1GUH7vWy1wjR6qF5vQnMun3XNQ=="], - "@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], + "@better-auth/utils": ["@better-auth/utils@0.4.2", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A=="], - "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + "@better-fetch/fetch": ["@better-fetch/fetch@1.3.1", "", {}, "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g=="], "@biomejs/biome": ["@biomejs/biome@2.4.7", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.7", "@biomejs/cli-darwin-x64": "2.4.7", "@biomejs/cli-linux-arm64": "2.4.7", "@biomejs/cli-linux-arm64-musl": "2.4.7", "@biomejs/cli-linux-x64": "2.4.7", "@biomejs/cli-linux-x64-musl": "2.4.7", "@biomejs/cli-win32-arm64": "2.4.7", "@biomejs/cli-win32-x64": "2.4.7" }, "bin": { "biome": "bin/biome" } }, "sha512-vXrgcmNGZ4lpdwZSpMf1hWw1aWS6B+SyeSYKTLrNsiUsAdSRN0J4d/7mF3ogJFbIwFFSOL3wT92Zzxia/d5/ng=="], @@ -261,23 +259,13 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], - "@fontsource-variable/geist": ["@fontsource-variable/geist@5.2.8", "", {}, "sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw=="], + "@fontsource-variable/geist": ["@fontsource-variable/geist@5.2.9", "", {}, "sha512-TP+QSBG3wxKGPE33CbMy/L0Nu3qvJ6Fy81Yc4LnQ95xH+i+cfEp8fyU8/kfV14YwszxIFPhnoMTbjL71waVpyQ=="], "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], - "@hugeicons/core-free-icons": ["@hugeicons/core-free-icons@4.0.0", "", {}, "sha512-bzfbKumv3ke3ajbe2MyXi9i0I/cdsZ6n/mO9EfIPNSL++pHLqs7nSGRIVUtjF4xrrEyVkfhxssv4Jek8DPA6gA=="], - - "@hugeicons/react": ["@hugeicons/react@1.1.6", "", { "peerDependencies": { "react": ">=16.0.0" } }, "sha512-c2LhXJMAW5wN1pC/smBXG0YPqUON6ceR/ZdXHCjEI9KvB+hjtqYjmzIxok5hAQOeXGz0WtORgCQMzqewFKAZwg=="], - - "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="], - - "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="], + "@hugeicons/core-free-icons": ["@hugeicons/core-free-icons@4.2.1", "", {}, "sha512-75jYZKYyA9VwS35YRmmGUGzFedbY+Fl0Vxx5FzXR2CGDlIhNRumFeVqaaKoClf2MeYEJwPAVMEL9RwCYtOgnSw=="], - "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="], - - "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], - - "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], + "@hugeicons/react": ["@hugeicons/react@1.1.7", "", { "peerDependencies": { "react": ">=16.0.0" } }, "sha512-0q1gekPJvNtIsfqi6alVdKfqsLz0PGiZWOzxJDxOd55UpFwcgohBFU4CwTcJ7kg4hO3kwXuzxrn/JEj280GX3g=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -289,31 +277,31 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@libsql/client": ["@libsql/client@0.17.0", "", { "dependencies": { "@libsql/core": "^0.17.0", "@libsql/hrana-client": "^0.9.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-TLjSU9Otdpq0SpKHl1tD1Nc9MKhrsZbCFGot3EbCxRa8m1E5R1mMwoOjKMMM31IyF7fr+hPNHLpYfwbMKNusmg=="], + "@libsql/client": ["@libsql/client@0.17.4", "", { "dependencies": { "@libsql/core": "^0.17.4", "@libsql/hrana-client": "^0.10.0", "js-base64": "^3.7.5", "libsql": "^0.5.28", "promise-limit": "^2.7.0" } }, "sha512-lYayFWasDV78A+TjlEhr6ubb3odBV6OHjb+wdp8VQcyWWAEIjuwbCHaraEUS4m4yWoo0BvZo96It4VdzZRmRWw=="], - "@libsql/core": ["@libsql/core@0.17.0", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-hnZRnJHiS+nrhHKLGYPoJbc78FE903MSDrFJTbftxo+e52X+E0Y0fHOCVYsKWcg6XgB7BbJYUrz/xEkVTSaipw=="], + "@libsql/core": ["@libsql/core@0.17.4", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-LqF9gIvnJ38nmAH1y/ChizHqDO/MO1wLgA96XrraulEEbqXxLjleSH92YWTolbuJKgPUmGu4aJk9W3UnAcxLOQ=="], - "@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.5.22", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4B8ZlX3nIDPndfct7GNe0nI3Yw6ibocEicWdC4fvQbSs/jdq/RC2oCsoJxJ4NzXkvktX70C1J4FcmmoBy069UA=="], + "@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.5.29", "", { "os": "darwin", "cpu": "arm64" }, "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A=="], - "@libsql/darwin-x64": ["@libsql/darwin-x64@0.5.22", "", { "os": "darwin", "cpu": "x64" }, "sha512-ny2HYWt6lFSIdNFzUFIJ04uiW6finXfMNJ7wypkAD8Pqdm6nAByO+Fdqu8t7sD0sqJGeUCiOg480icjyQ2/8VA=="], + "@libsql/darwin-x64": ["@libsql/darwin-x64@0.5.29", "", { "os": "darwin", "cpu": "x64" }, "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ=="], - "@libsql/hrana-client": ["@libsql/hrana-client@0.9.0", "", { "dependencies": { "@libsql/isomorphic-ws": "^0.1.5", "cross-fetch": "^4.0.0", "js-base64": "^3.7.5", "node-fetch": "^3.3.2" } }, "sha512-pxQ1986AuWfPX4oXzBvLwBnfgKDE5OMhAdR/5cZmRaB4Ygz5MecQybvwZupnRz341r2CtFmbk/BhSu7k2Lm+Jw=="], + "@libsql/hrana-client": ["@libsql/hrana-client@0.10.0", "", { "dependencies": { "@libsql/isomorphic-ws": "^0.1.5", "js-base64": "^3.7.5" } }, "sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw=="], "@libsql/isomorphic-ws": ["@libsql/isomorphic-ws@0.1.5", "", { "dependencies": { "@types/ws": "^8.5.4", "ws": "^8.13.0" } }, "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg=="], - "@libsql/linux-arm-gnueabihf": ["@libsql/linux-arm-gnueabihf@0.5.22", "", { "os": "linux", "cpu": "arm" }, "sha512-3Uo3SoDPJe/zBnyZKosziRGtszXaEtv57raWrZIahtQDsjxBVjuzYQinCm9LRCJCUT5t2r5Z5nLDPJi2CwZVoA=="], + "@libsql/linux-arm-gnueabihf": ["@libsql/linux-arm-gnueabihf@0.5.29", "", { "os": "linux", "cpu": "arm" }, "sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ=="], - "@libsql/linux-arm-musleabihf": ["@libsql/linux-arm-musleabihf@0.5.22", "", { "os": "linux", "cpu": "arm" }, "sha512-LCsXh07jvSojTNJptT9CowOzwITznD+YFGGW+1XxUr7fS+7/ydUrpDfsMX7UqTqjm7xG17eq86VkWJgHJfvpNg=="], + "@libsql/linux-arm-musleabihf": ["@libsql/linux-arm-musleabihf@0.5.29", "", { "os": "linux", "cpu": "arm" }, "sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg=="], - "@libsql/linux-arm64-gnu": ["@libsql/linux-arm64-gnu@0.5.22", "", { "os": "linux", "cpu": "arm64" }, "sha512-KSdnOMy88c9mpOFKUEzPskSaF3VLflfSUCBwas/pn1/sV3pEhtMF6H8VUCd2rsedwoukeeCSEONqX7LLnQwRMA=="], + "@libsql/linux-arm64-gnu": ["@libsql/linux-arm64-gnu@0.5.29", "", { "os": "linux", "cpu": "arm64" }, "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w=="], - "@libsql/linux-arm64-musl": ["@libsql/linux-arm64-musl@0.5.22", "", { "os": "linux", "cpu": "arm64" }, "sha512-mCHSMAsDTLK5YH//lcV3eFEgiR23Ym0U9oEvgZA0667gqRZg/2px+7LshDvErEKv2XZ8ixzw3p1IrBzLQHGSsw=="], + "@libsql/linux-arm64-musl": ["@libsql/linux-arm64-musl@0.5.29", "", { "os": "linux", "cpu": "arm64" }, "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg=="], - "@libsql/linux-x64-gnu": ["@libsql/linux-x64-gnu@0.5.22", "", { "os": "linux", "cpu": "x64" }, "sha512-kNBHaIkSg78Y4BqAdgjcR2mBilZXs4HYkAmi58J+4GRwDQZh5fIUWbnQvB9f95DkWUIGVeenqLRFY2pcTmlsew=="], + "@libsql/linux-x64-gnu": ["@libsql/linux-x64-gnu@0.5.29", "", { "os": "linux", "cpu": "x64" }, "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg=="], - "@libsql/linux-x64-musl": ["@libsql/linux-x64-musl@0.5.22", "", { "os": "linux", "cpu": "x64" }, "sha512-UZ4Xdxm4pu3pQXjvfJiyCzZop/9j/eA2JjmhMaAhe3EVLH2g11Fy4fwyUp9sT1QJYR1kpc2JLuybPM0kuXv/Tg=="], + "@libsql/linux-x64-musl": ["@libsql/linux-x64-musl@0.5.29", "", { "os": "linux", "cpu": "x64" }, "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w=="], - "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.22", "", { "os": "win32", "cpu": "x64" }, "sha512-Fj0j8RnBpo43tVZUVoNK6BV/9AtDUM5S7DF3LB4qTYg1LMSZqi3yeCneUTLJD6XomQJlZzbI4mst89yspVSAnA=="], + "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.29", "", { "os": "win32", "cpu": "x64" }, "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "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" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], @@ -331,8 +319,6 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], - "@mswjs/interceptors": ["@mswjs/interceptors@0.41.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], @@ -357,49 +343,43 @@ "@oozcitak/util": ["@oozcitak/util@10.0.0", "", {}, "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA=="], - "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], - - "@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="], - - "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], - "@oxc-project/types": ["@oxc-project/types@0.129.0", "", {}, "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg=="], + "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0", "", { "os": "linux", "cpu": "arm" }, "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0", "", { "os": "none", "cpu": "arm64" }, "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.3", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0", "", {}, "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ=="], "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], @@ -421,35 +401,35 @@ "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], - "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], + "@tailwindcss/node": ["@tailwindcss/node@4.3.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "5.21.6", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.1" } }, "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.1", "@tailwindcss/oxide-darwin-arm64": "4.3.1", "@tailwindcss/oxide-darwin-x64": "4.3.1", "@tailwindcss/oxide-freebsd-x64": "4.3.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", "@tailwindcss/oxide-linux-x64-musl": "4.3.1", "@tailwindcss/oxide-wasm32-wasi": "4.3.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" } }, "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.1", "", { "os": "android", "cpu": "arm64" }, "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.1", "", { "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.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA=="], - "@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.3.1", "", { "dependencies": { "@tailwindcss/node": "4.3.1", "@tailwindcss/oxide": "4.3.1", "tailwindcss": "4.3.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ=="], "@tanstack/devtools": ["@tanstack/devtools@0.7.0", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/keyboard": "^1.3.3", "@solid-primitives/resize-observer": "^2.1.3", "@tanstack/devtools-client": "0.0.3", "@tanstack/devtools-event-bus": "0.3.3", "@tanstack/devtools-ui": "0.4.4", "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-AlAoCqJhWLg9GBEaoV1g/j+X/WA1aJSWOsekxeuZpYeS2hdVuKAjj04KQLUMJhtLfNl2s2E+TCj7ZRtWyY3U4w=="], @@ -463,53 +443,55 @@ "@tanstack/devtools-vite": ["@tanstack/devtools-vite@0.3.12", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/generator": "^7.28.3", "@babel/parser": "^7.28.4", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@tanstack/devtools-client": "0.0.5", "@tanstack/devtools-event-bus": "0.3.3", "chalk": "^5.6.2", "launch-editor": "^2.11.1", "picomatch": "^4.0.3" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0" } }, "sha512-fGJgu4xUhKmGk+a+/aHD8l5HKVk6+ObA+6D3YC3xCXbai/YmaGhztqcZf1tKUqjZyYyQLHsjqmKzvJgVpQP1jw=="], - "@tanstack/history": ["@tanstack/history@1.161.4", "", {}, "sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww=="], + "@tanstack/history": ["@tanstack/history@1.162.0", "", {}, "sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA=="], - "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], + "@tanstack/query-core": ["@tanstack/query-core@5.101.0", "", {}, "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow=="], "@tanstack/react-devtools": ["@tanstack/react-devtools@0.7.11", "", { "dependencies": { "@tanstack/devtools": "0.7.0" }, "peerDependencies": { "@types/react": ">=16.8", "@types/react-dom": ">=16.8", "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-a2Lmz8x+JoDrsU6f7uKRcyyY+k8mA/n5mb9h7XJ3Fz/y3+sPV9t7vAW1s5lyNkQyyDt6V1Oim99faLthoJSxMw=="], - "@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="], + "@tanstack/react-query": ["@tanstack/react-query@5.101.0", "", { "dependencies": { "@tanstack/query-core": "5.101.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg=="], - "@tanstack/react-router": ["@tanstack/react-router@1.166.7", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/react-store": "^0.9.1", "@tanstack/router-core": "1.166.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-LLcXu2nrCn2WL+w0YAbg3CRZIIO2cYVSC3y+ZYlFBxBs4hh8eoNP1EWFvRLZGCFYpqON7x6qUf1u0W7tH0cJJw=="], + "@tanstack/react-router": ["@tanstack/react-router@1.170.16", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.171.13", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-w6eq1IJklujs1tESazaK/FxH0+H2l8vm/QPuu1cD3oRW/ubgKneQpd7b64ti/8gUyEimzimJQZDmJr6YHfP5+g=="], - "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.166.7", "", { "dependencies": { "@tanstack/router-devtools-core": "1.166.7" }, "peerDependencies": { "@tanstack/react-router": "^1.166.7", "@tanstack/router-core": "^1.166.7", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-sAh3gA3wkMvUI6rRLPW4lfP0XxeEA0wrlv4tW1cinb7eoD3avcdKwiE9jhQ3DgFlhVsHa9fa3AKxH46Y/d/e1g=="], + "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.167.0", "", { "dependencies": { "@tanstack/router-devtools-core": "1.168.0" }, "peerDependencies": { "@tanstack/react-router": "^1.170.0", "@tanstack/router-core": "^1.170.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-nGw095EG7IHx0h5NtlEmzf6vcCTaFNPWdTSuDKazajhN0ct/v/TkekJ9J6KYUCeV1a8/2ZmToc58M+0rrOyn7w=="], - "@tanstack/react-router-ssr-query": ["@tanstack/react-router-ssr-query@1.166.7", "", { "dependencies": { "@tanstack/router-ssr-query-core": "1.166.7" }, "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/react-query": ">=5.90.0", "@tanstack/react-router": ">=1.127.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-GiPV0JuygPlkyH0SYhAzks2FO5Rdym15ScXLQXjOVziaUBum6as6TXYXk486jtdz9JHfxSIoUBgBjCBO1c+XZg=="], + "@tanstack/react-router-ssr-query": ["@tanstack/react-router-ssr-query@1.167.1", "", { "dependencies": { "@tanstack/router-ssr-query-core": "1.169.1" }, "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/react-query": ">=5.90.0", "@tanstack/react-router": ">=1.127.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-W9j5JPnBikyafvuUfykFfHIWod58OAbAAa5leNkXBcoDoocghMmu6w9uZOmUZvAWT7CSvgj5tBUtF7CM2OoHXQ=="], - "@tanstack/react-start": ["@tanstack/react-start@1.166.8", "", { "dependencies": { "@tanstack/react-router": "1.166.7", "@tanstack/react-start-client": "1.166.7", "@tanstack/react-start-server": "1.166.7", "@tanstack/router-utils": "^1.161.4", "@tanstack/start-client-core": "1.166.7", "@tanstack/start-plugin-core": "1.166.8", "@tanstack/start-server-core": "1.166.7", "pathe": "^2.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" } }, "sha512-2s8Je9/XdN3PR5lfK6Pqo5mcw0aTdBr7Y8aScfS2v70DRb7ZC6yGpe3+6qqOfAjNJ52fsV+4Y8Ad2nZY/IHZCA=="], + "@tanstack/react-start": ["@tanstack/react-start@1.168.26", "", { "dependencies": { "@tanstack/react-router": "1.170.16", "@tanstack/react-start-client": "1.168.14", "@tanstack/react-start-rsc": "0.1.25", "@tanstack/react-start-server": "1.167.20", "@tanstack/router-utils": "1.162.2", "@tanstack/start-client-core": "1.170.12", "@tanstack/start-plugin-core": "1.171.18", "@tanstack/start-server-core": "1.169.15", "pathe": "^2.0.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "vite"] }, "sha512-ZzNecqKWC0p2643/kIcFUdsMrXZ8A4dXm4Yfe9zV/Y0u14MtSkh/Sk76RQYIU5S84VyDyKhHo0Ffh8HQbLdvFw=="], - "@tanstack/react-start-client": ["@tanstack/react-start-client@1.166.7", "", { "dependencies": { "@tanstack/react-router": "1.166.7", "@tanstack/router-core": "1.166.7", "@tanstack/start-client-core": "1.166.7", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-s5hUibmThU1D0lEXX1ecZ5M1ANB4YBJ5gLUjuR3SXhBon3S5pvNmuSi9blrlL67nyGqe9y0w2+pTyJApobCFtw=="], + "@tanstack/react-start-client": ["@tanstack/react-start-client@1.168.14", "", { "dependencies": { "@tanstack/react-router": "1.170.16", "@tanstack/router-core": "1.171.13", "@tanstack/start-client-core": "1.170.12" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-oaz43fdOhBWfOPsdLkp3IejwYEXRyeaZ4CYexOPZx3eVXzm4LLozvNkkkBE9aje1sM5MVC2Yo6+cyv2HdVzkHQ=="], - "@tanstack/react-start-server": ["@tanstack/react-start-server@1.166.7", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/react-router": "1.166.7", "@tanstack/router-core": "1.166.7", "@tanstack/start-client-core": "1.166.7", "@tanstack/start-server-core": "1.166.7" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-36yHsjophX/8FZ6fnVXLuqWnpks8/kNcCWuIzSmFVMBSv70hZvEibMVlpCiuTXfv6kcKQuVqYBpF3+Kx98XaQA=="], + "@tanstack/react-start-rsc": ["@tanstack/react-start-rsc@0.1.25", "", { "dependencies": { "@tanstack/react-router": "1.170.16", "@tanstack/router-core": "1.171.13", "@tanstack/router-utils": "1.162.2", "@tanstack/start-client-core": "1.170.12", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-plugin-core": "1.171.18", "@tanstack/start-server-core": "1.169.15", "@tanstack/start-storage-context": "1.167.15", "pathe": "^2.0.3" }, "peerDependencies": { "@rspack/core": ">=2.0.0-0", "@vitejs/plugin-rsc": ">=0.5.20", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "react-server-dom-rspack": ">=0.0.2" }, "optionalPeers": ["@rspack/core", "@vitejs/plugin-rsc", "react-server-dom-rspack"] }, "sha512-Rwm6cjcS148y2XAebr8jyrwU0SqsRSkW4/CuXesoHg+G3IOnVRHV0HOylJfnznUTVuH1nVhfQRPI5uWPJaw2TA=="], - "@tanstack/react-store": ["@tanstack/react-store@0.9.2", "", { "dependencies": { "@tanstack/store": "0.9.2", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Vt5usJE5sHG/cMechQfmwvwne6ktGCELe89Lmvoxe3LKRoFrhPa8OCKWs0NliG8HTJElEIj7PLtaBQIcux5pAQ=="], + "@tanstack/react-start-server": ["@tanstack/react-start-server@1.167.20", "", { "dependencies": { "@tanstack/react-router": "1.170.16", "@tanstack/router-core": "1.171.13", "@tanstack/start-server-core": "1.169.15" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-hg9xVI6yNDu+6NCalKz1h3Ea18HZEUu35i/ZMJz1WadwqcArLMp41nM1GNhOAtGIdpycrp9tT7ccqc9zuRMRpQ=="], - "@tanstack/router-core": ["@tanstack/router-core@1.166.7", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/store": "^0.9.1", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-MCc8wYIIcxmbeidM8PL2QeaAjUIHyhEDIZPW6NGfn/uwvyi+K2ucn3AGCxxcXl4JGGm0Mx9+7buYl1v3HdcFrg=="], + "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], - "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.166.7", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.166.7", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-/OGLZlrw5NSNd9/PTL8vPSpmjxIbXNoeJATMHlU3YLCBVBtLx41CHIRc7OLkjyfVFJ4Sq7Pq+2/YH8PChShefg=="], + "@tanstack/router-core": ["@tanstack/router-core@1.171.13", "", { "dependencies": { "@tanstack/history": "1.162.0", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-+NOwEj1kO/6IGmpHRIZHasYxYWpyBQGNIZAST9aNrk9Q3YlU9SgqVnl1pbLa9qAKfeNdXQIRve0RQb/0kyDeDA=="], - "@tanstack/router-generator": ["@tanstack/router-generator@1.166.7", "", { "dependencies": { "@tanstack/router-core": "1.166.7", "@tanstack/router-utils": "1.161.4", "@tanstack/virtual-file-routes": "1.161.4", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-lBI0VS7J1zMrJhfvT+3FMq9jPdOrJ3VgciPXyYvZBF/a9Mr8T94MU78PqrBNuJbYh7qCFO14ZhArUFqkYGuozQ=="], + "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.168.0", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/router-core": "^1.170.0", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-wQoQhlBK7nlZgqzaqdYXKWNTpdHdsaREdaPhFZVH0/Ador+F+eM3/NF2i3f2LPeS0GgKraZUQXe1Q/1+KHyEYg=="], - "@tanstack/router-plugin": ["@tanstack/router-plugin@1.166.7", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.166.7", "@tanstack/router-generator": "1.166.7", "@tanstack/router-utils": "1.161.4", "@tanstack/virtual-file-routes": "1.161.4", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.166.7", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-R06qe5UwApb/u02wDITVxN++6QE4xsLFQCr029VZ+4V8gyIe35kr8UCg3Jiyl6D5GXxhj62U2Ei8jccdkQaivw=="], + "@tanstack/router-generator": ["@tanstack/router-generator@1.167.17", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.13", "@tanstack/router-utils": "1.162.2", "@tanstack/virtual-file-routes": "1.162.0", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^4.4.3" } }, "sha512-xtB9tB2Ws0tWR6Pi7nc3Qk9IYgoh1mQCKWjHqIl9tf6BNUpKoqniJoPAQ4+LGrK8FeZYU0o0p/qlZEyj9FAulA=="], - "@tanstack/router-ssr-query-core": ["@tanstack/router-ssr-query-core@1.166.7", "", { "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/router-core": ">=1.127.0" } }, "sha512-NXUcZtT5uR67dRPl7s6/17ThXtXERCQN10STO5JRNuvlzwblY9e/QOr04W7Q8IxlhrthL21J1PPuIZTdEBBPEQ=="], + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.168.18", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.13", "@tanstack/router-generator": "1.167.17", "@tanstack/router-utils": "1.162.2", "chokidar": "^5.0.0", "unplugin": "^3.0.0", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.170.15", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-MofS28/axfnfnhOD2RSgJEaU882aX5RsAzhGz5Vc4XhAmvCjy919u9JrNs4QsTWFbTD1P7IJ8WFlFVsrg0pStg=="], - "@tanstack/router-utils": ["@tanstack/router-utils@1.161.4", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-r8TpjyIZoqrXXaf2DDyjd44gjGBoyE+/oEaaH68yLI9ySPO1gUWmQENZ1MZnmBnpUGN24NOZxdjDLc8npK0SAw=="], + "@tanstack/router-ssr-query-core": ["@tanstack/router-ssr-query-core@1.169.1", "", { "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/router-core": ">=1.127.0" } }, "sha512-rngux8s/3mPQzcjLYDLkNU31coYVyCgrVTfpdwqUdY5jIEHqGTXrO73DTkPR1PppwYUeVhmNCgl8TctRcnupjg=="], - "@tanstack/start-client-core": ["@tanstack/start-client-core@1.166.7", "", { "dependencies": { "@tanstack/router-core": "1.166.7", "@tanstack/start-fn-stubs": "1.161.4", "@tanstack/start-storage-context": "1.166.7", "seroval": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-GYSBqJF6yutorSSiOYtOuklAAocvzDM9hijrBSegzxeKJtxUVejIb6M1txH4c+dfikcwaGo6/lnKJWzUE+SjhQ=="], + "@tanstack/router-utils": ["@tanstack/router-utils@1.162.2", "", { "dependencies": { "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ=="], - "@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.161.4", "", {}, "sha512-b8s6iSQ+ny0P4lGK0n3DKaL6EI7SECG0/89svDeYieVw2+MaFOJVcQo3rU3BUvmuOcIkgkE5IhdzkmzPXH6yfA=="], + "@tanstack/start-client-core": ["@tanstack/start-client-core@1.170.12", "", { "dependencies": { "@tanstack/router-core": "1.171.13", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-storage-context": "1.167.15", "seroval": "^1.5.4" } }, "sha512-gwtZRMPUIAxmDV2AIQUhC0kSW262SV7BkHXEgy5B1woHQdrdsELuGOdJwdweLxrjyefORxk+9MYGqDY0Cxn0bw=="], - "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.166.8", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.0-beta.40", "@tanstack/router-core": "1.166.7", "@tanstack/router-generator": "1.166.7", "@tanstack/router-plugin": "1.166.7", "@tanstack/router-utils": "1.161.4", "@tanstack/start-client-core": "1.166.7", "@tanstack/start-server-core": "1.166.7", "cheerio": "^1.0.0", "exsolve": "^1.0.7", "pathe": "^2.0.3", "picomatch": "^4.0.3", "source-map": "^0.7.6", "srvx": "^0.11.9", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^3.24.2" }, "peerDependencies": { "vite": ">=7.0.0" } }, "sha512-mnOzxHs+s/AWPylgmo/rjHNFTdK9hsxyaWOhQJycih8VGSeWOtxXAdBDLjw6+81FQjAyudp/wy/s6VBtElDHGQ=="], + "@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.162.0", "", {}, "sha512-QWfUZ3Yo923tdQn38LyKMU8rcTw69zc+T4dAvgTWV4O56SqFRsGfS0lSWIMhJRwXIx/bvdi7nTUBDdZtTHtpTQ=="], - "@tanstack/start-server-core": ["@tanstack/start-server-core@1.166.7", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/router-core": "1.166.7", "@tanstack/start-client-core": "1.166.7", "@tanstack/start-storage-context": "1.166.7", "h3-v2": "npm:h3@2.0.1-rc.16", "seroval": "^1.4.2", "tiny-invariant": "^1.3.3" } }, "sha512-LWUhi70PuiO2PCYPlXnK11ASw9TA67qo/PO4omSB5zD5ZE0W8PmMBEo1sHY1bc2iYGWRHWIdcPbMFZ2pm2dHkQ=="], + "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.171.18", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.13", "@tanstack/router-generator": "1.167.17", "@tanstack/router-plugin": "1.168.18", "@tanstack/router-utils": "1.162.2", "@tanstack/start-server-core": "1.169.15", "exsolve": "^1.0.7", "lightningcss": "^1.32.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "seroval": "^1.5.4", "source-map": "^0.7.6", "srvx": "^0.11.9", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "vite"] }, "sha512-weuOOjRD03BiKcF9NYKL5QADEQOp3yGHb0qIsOX42ENz5XUE4in2fXcVYHjSwwpzs+XGhWIFLp5J385pOVPuZQ=="], - "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.166.7", "", { "dependencies": { "@tanstack/router-core": "1.166.7" } }, "sha512-u6gIqf5JAxMBaCUdF34fa0mQEDatj44kJ2i9U3RtI220TgwIxUKCYxhxUpXxtYBU3whCZ07EYwBpKeWxrIhXZQ=="], + "@tanstack/start-server-core": ["@tanstack/start-server-core@1.169.15", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/router-core": "1.171.13", "@tanstack/start-client-core": "1.170.12", "@tanstack/start-storage-context": "1.167.15", "fetchdts": "^0.1.6", "h3-v2": "npm:h3@2.0.1-rc.20", "seroval": "^1.5.4" } }, "sha512-V8ie2G2Ecb3Nklk8/QuKzulxb+tDUqgz6rjJe8Isdp4iKVXZu/TscItkUP/4Q5Bu7W4gUy3P25O6soC1oW1SXQ=="], - "@tanstack/store": ["@tanstack/store@0.9.2", "", {}, "sha512-K013lUJEFJK2ofFQ/hZKJUmCnpcV00ebLyOyFOWQvyQHUOZp/iYO84BM6aOGiV81JzwbX0APTVmW8YI7yiG5oA=="], + "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.167.15", "", { "dependencies": { "@tanstack/router-core": "1.171.13" } }, "sha512-Jy0q4vdG6pv76N92+X+ag3fuOV2zINQagYyMN1/es7tPI1vzpKECIU8AqHqzI6ahkwaph7XDvmfUkiLJ3i4LOA=="], - "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.4", "", {}, "sha512-42WoRePf8v690qG8yGRe/YOh+oHni9vUaUUfoqlS91U2scd3a5rkLtVsc6b7z60w3RogH0I00vdrC5AaeiZ18w=="], + "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], + + "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="], "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], @@ -545,14 +527,12 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="], + "@types/node": ["@types/node@22.20.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g=="], - "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], - "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], @@ -569,26 +549,24 @@ "@uploadthing/shared": ["@uploadthing/shared@7.1.10", "", { "dependencies": { "@uploadthing/mime-types": "0.3.6", "effect": "3.17.7", "sqids": "^0.3.0" } }, "sha512-R/XSA3SfCVnLIzFpXyGaKPfbwlYlWYSTuGjTFHuJhdAomuBuhopAHLh2Ois5fJibAHzi02uP1QCKbgTAdmArqg=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="], - "@vitest/expect": ["@vitest/expect@4.1.6", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.6", "@vitest/utils": "4.1.6", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg=="], + "@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], - "@vitest/mocker": ["@vitest/mocker@4.1.6", "", { "dependencies": { "@vitest/spy": "4.1.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ=="], + "@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.6", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.9", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A=="], - "@vitest/runner": ["@vitest/runner@4.1.6", "", { "dependencies": { "@vitest/utils": "4.1.6", "pathe": "^2.0.3" } }, "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA=="], + "@vitest/runner": ["@vitest/runner@4.1.9", "", { "dependencies": { "@vitest/utils": "4.1.9", "pathe": "^2.0.3" } }, "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg=="], - "@vitest/snapshot": ["@vitest/snapshot@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "@vitest/utils": "4.1.6", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw=="], + "@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], - "@vitest/spy": ["@vitest/spy@4.1.6", "", {}, "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg=="], + "@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], - "@vitest/utils": ["@vitest/utils@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ=="], + "@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], @@ -617,9 +595,9 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.7", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw=="], - "better-auth": ["better-auth@1.5.5", "", { "dependencies": { "@better-auth/core": "1.5.5", "@better-auth/drizzle-adapter": "1.5.5", "@better-auth/kysely-adapter": "1.5.5", "@better-auth/memory-adapter": "1.5.5", "@better-auth/mongo-adapter": "1.5.5", "@better-auth/prisma-adapter": "1.5.5", "@better-auth/telemetry": "1.5.5", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-GpVPaV1eqr3mOovKfghJXXk6QvlcVeFbS3z+n+FPDid5rK/2PchnDtiaVCzWyXA9jH2KkirOfl+JhAUvnja0Eg=="], + "better-auth": ["better-auth@1.6.20", "", { "dependencies": { "@better-auth/core": "1.6.20", "@better-auth/drizzle-adapter": "1.6.20", "@better-auth/kysely-adapter": "1.6.20", "@better-auth/memory-adapter": "1.6.20", "@better-auth/mongo-adapter": "1.6.20", "@better-auth/prisma-adapter": "1.6.20", "@better-auth/telemetry": "1.6.20", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.6", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-fSpGHGRKiGRiYVd3QTQtuVZ8oxpiSe/7ip0Rpvt/Sy8zQbEbVKUPMOhE0gLXg+FjqTUsIo7582hxUYxtEcqUpA=="], - "better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], + "better-call": ["better-call@1.3.6", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-no1jI+h6Bkxs1NVBo4rONbVIzsPjZ8IUu7IHaJBiFwVX1XEQGN8KpHots5fSWmXe9nNyLuLIcgx6WEUcE6EDaA=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], @@ -627,8 +605,6 @@ "body-parser": ["body-parser@2.2.2", "", { "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" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], - "brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -655,10 +631,6 @@ "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], - - "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], - "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], @@ -669,18 +641,10 @@ "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], - - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], @@ -691,9 +655,9 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], + "cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], @@ -701,18 +665,12 @@ "cosmiconfig": ["cosmiconfig@9.0.1", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ=="], - "cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "crossws": ["crossws@0.4.5", "", { "peerDependencies": { "srvx": ">=0.11.5" }, "optionalPeers": ["srvx"] }, "sha512-wUR89x/Rw7/8t+vn0CmGDYM9TD6VtARGb0LD5jq2wjtMy1vCP4M+sm6N6TigWeTYvnA8MoW29NqqXD0ep0rfBA=="], - "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], - "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], - "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], - "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "cssstyle": ["cssstyle@5.3.7", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ=="], @@ -775,19 +733,11 @@ "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], - - "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], - - "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], - - "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], - "drizzle-kit": ["drizzle-kit@0.31.9", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg=="], + "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], - "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], + "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -803,9 +753,7 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], - - "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="], + "enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="], "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], @@ -827,8 +775,6 @@ "esbuild": ["esbuild@0.25.12", "", { "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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], @@ -869,6 +815,8 @@ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "fetchdts": ["fetchdts@0.1.7", "", {}, "sha512-YoZjBdafyLIop9lSxXVI33oLD5kN31q4Td+CasofLLYeLXRFeOsuOw0Uo+XNRi9PZlbfdlN2GmRtm4tCEQ9/KA=="], + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], "file-selector": ["file-selector@0.6.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw=="], @@ -883,7 +831,7 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "framer-motion": ["framer-motion@12.39.0", "", { "dependencies": { "motion-dom": "^12.39.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-+vnLfzrv0MzjLzNl+nvNvR7jdg3q4cxxjz/YvzfifHl0TREtL00cs1RoMTxs+1PzLiEqZGV6gYsBY0oEAYZ24w=="], + "framer-motion": ["framer-motion@12.40.0", "", { "dependencies": { "motion-dom": "^12.40.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -895,12 +843,8 @@ "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="], - "fzf": ["fzf@0.5.2", "", {}, "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q=="], - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -923,26 +867,20 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "graphql": ["graphql@16.13.1", "", {}, "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ=="], - "h3": ["h3@2.0.1-rc.22", "", { "dependencies": { "rou3": "^0.8.1", "srvx": "^0.11.15" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-Esv0DMIuPkCTSWCA0vO73vcTqwzH1wjSrAO1TXNu/K3up1sZHa9EKMapbmxCDYBeymC3fVTk4qxp7ogQWQ+KgA=="], - "h3-v2": ["h3@2.0.1-rc.16", "", { "dependencies": { "rou3": "^0.8.0", "srvx": "^0.11.9" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-h+pjvyujdo9way8qj6FUbhaQcHlR8FEq65EhTX9ViT5pK8aLj68uFl4hBkF+hsTJAH+H1END2Yv6hTIsabGfag=="], + "h3-v2": ["h3@2.0.1-rc.20", "", { "dependencies": { "rou3": "^0.8.1", "srvx": "^0.11.13" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg=="], "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], - "hono": ["hono@4.12.7", "", {}, "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw=="], "hookable": ["hookable@6.1.1", "", {}, "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ=="], "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], - "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], - "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], @@ -977,8 +915,6 @@ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], @@ -987,8 +923,6 @@ "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], - "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="], - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "is-obj": ["is-obj@3.0.0", "", {}, "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ=="], @@ -1039,11 +973,11 @@ "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], - "kysely": ["kysely@0.28.11", "", {}, "sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg=="], + "kysely": ["kysely@0.29.2", "", {}, "sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg=="], "launch-editor": ["launch-editor@2.13.1", "", { "dependencies": { "picocolors": "^1.1.1", "shell-quote": "^1.8.3" } }, "sha512-lPSddlAAluRKJ7/cjRFoXUFzaX7q/YKI7yPHuEvSJVqoXvFnJov1/Ud87Aa4zULIbA9Nja4mSPK8l0z/7eV2wA=="], - "libsql": ["libsql@0.5.22", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.22", "@libsql/darwin-x64": "0.5.22", "@libsql/linux-arm-gnueabihf": "0.5.22", "@libsql/linux-arm-musleabihf": "0.5.22", "@libsql/linux-arm64-gnu": "0.5.22", "@libsql/linux-arm64-musl": "0.5.22", "@libsql/linux-x64-gnu": "0.5.22", "@libsql/linux-x64-musl": "0.5.22", "@libsql/win32-x64-msvc": "0.5.22" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-NscWthMQt7fpU8lqd7LXMvT9pi+KhhmTHAJWUB/Lj6MWa0MKFv0F2V4C6WKKpjCVZl0VwcDz4nOI3CyaT1DDiA=="], + "libsql": ["libsql@0.5.29", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.29", "@libsql/darwin-x64": "0.5.29", "@libsql/linux-arm-gnueabihf": "0.5.29", "@libsql/linux-arm-musleabihf": "0.5.29", "@libsql/linux-arm64-gnu": "0.5.29", "@libsql/linux-arm64-musl": "0.5.29", "@libsql/linux-x64-gnu": "0.5.29", "@libsql/linux-x64-musl": "0.5.29", "@libsql/win32-x64-msvc": "0.5.29" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "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" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -1115,9 +1049,9 @@ "mongodb-connection-string-url": ["mongodb-connection-string-url@7.0.1", "", { "dependencies": { "@types/whatwg-url": "^13.0.0", "whatwg-url": "^14.1.0" } }, "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ=="], - "motion": ["motion@12.39.0", "", { "dependencies": { "framer-motion": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-H4a+Ze+a9j+/NTla5ezfb/g9vmIOxC+viDj++NGDZyTZkdRKjiOz3kSv6TalRWM8ZmD2y/CfC6TkQc97ybyqSA=="], + "motion": ["motion@12.40.0", "", { "dependencies": { "framer-motion": "^12.40.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-yjrHUrBFW6kQvjJwRsoiPSAhC5tRwRqNGJWmiJ4CrGnbKp0V88AdzkhBmDoqIsIPfarOe0Uddd37Xq43/gIocA=="], - "motion-dom": ["motion-dom@12.39.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-Xn7aAcGDhco/JZTXOub64UmaYn73C6J1Po7Fk+8EvkJsNGTqfhon6UJY53vJKXW5v5Zl8HrYsVxv6oPXeGoGLQ=="], + "motion-dom": ["motion-dom@12.40.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="], "motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], @@ -1127,13 +1061,9 @@ "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], - "msw": ["msw@2.12.10", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw=="], - "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], - "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "nanoid": ["nanoid@3.3.14", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ=="], "nanostores": ["nanostores@1.1.1", "", {}, "sha512-EYJqS25r2iBeTtGQCHidXl1VfZ1jXM7Q04zXJOrMlxVVmD0ptxJaNux92n1mJ7c5lN3zTq12MhH/8x59nP+qmg=="], @@ -1155,8 +1085,6 @@ "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], - "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -1183,10 +1111,6 @@ "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], - "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], - - "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], @@ -1195,10 +1119,6 @@ "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], - "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], - - "parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="], - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], @@ -1207,7 +1127,7 @@ "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -1217,7 +1137,7 @@ "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], @@ -1247,9 +1167,9 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], - "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], @@ -1265,8 +1185,6 @@ "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], @@ -1277,11 +1195,9 @@ "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - "rettime": ["rettime@0.10.1", "", {}, "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "rolldown": ["rolldown@1.0.0", "", { "dependencies": { "@oxc-project/types": "=0.129.0", "@rolldown/pluginutils": "1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0", "@rolldown/binding-darwin-arm64": "1.0.0", "@rolldown/binding-darwin-x64": "1.0.0", "@rolldown/binding-freebsd-x64": "1.0.0", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", "@rolldown/binding-linux-arm64-gnu": "1.0.0", "@rolldown/binding-linux-arm64-musl": "1.0.0", "@rolldown/binding-linux-ppc64-gnu": "1.0.0", "@rolldown/binding-linux-s390x-gnu": "1.0.0", "@rolldown/binding-linux-x64-gnu": "1.0.0", "@rolldown/binding-linux-x64-musl": "1.0.0", "@rolldown/binding-openharmony-arm64": "1.0.0", "@rolldown/binding-wasm32-wasi": "1.0.0", "@rolldown/binding-win32-arm64-msvc": "1.0.0", "@rolldown/binding-win32-x64-msvc": "1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA=="], + "rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="], "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], @@ -1301,9 +1217,9 @@ "send": ["send@1.2.1", "", { "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" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - "seroval": ["seroval@1.5.1", "", {}, "sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA=="], + "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], - "seroval-plugins": ["seroval-plugins@1.5.1", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw=="], + "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], @@ -1311,7 +1227,7 @@ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "shadcn": ["shadcn@4.0.6", "", { "dependencies": { "@antfu/ni": "^25.0.0", "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-20M3ZsQPas+0QLVzrfrya2V3nryQiIPQsnJaOJg+Ylk24GtJa1vXGOLB7xwKuvWoMzM0YSLuCaoibW/NuHdDyA=="], + "shadcn": ["shadcn@4.11.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-UV0cchFea9hO7poV1CuEP0wvmYjpAqcxCKdy23bndl2Du2ARtDs8A4xdzfhUjDBeOW1nNpJ6lXmsEpsply2SfQ=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -1355,8 +1271,6 @@ "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], - "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], - "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "stringify-object": ["stringify-object@5.0.0", "", { "dependencies": { "get-own-enumerable-keys": "^1.0.0", "is-obj": "^3.0.0", "is-regexp": "^3.1.0" } }, "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg=="], @@ -1369,25 +1283,19 @@ "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], - "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], - - "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], - - "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], + "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], - "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], + "tailwindcss": ["tailwindcss@4.3.1", "", {}, "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q=="], - "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], - "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], - "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], @@ -1413,8 +1321,6 @@ "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], - "type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="], - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -1423,8 +1329,6 @@ "ultracite": ["ultracite@7.3.2", "", { "dependencies": { "@clack/prompts": "^1.1.0", "commander": "^14.0.3", "deepmerge": "^4.3.1", "glob": "^13.0.6", "jsonc-parser": "^3.3.1", "nypm": "^0.6.5" }, "peerDependencies": { "oxlint": "^1.0.0" }, "optionalPeers": ["oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-JV2TdeBDc1aaj3wFzXLjRcBlUZkXBdShMLAjHh9YLK0rXPSK42CzHqfWXUfibFUAUiUGNwoOFnIpS45EaJAcdg=="], - "undici": ["undici@7.24.0", "", {}, "sha512-jxytwMHhsbdpBXxLAcuu0fzlQeXCNnWdDyRHpvWsUl8vd98UwYdl9YTyn8/HcpcJPC3pwUveefsa3zTxyD/ERg=="], - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], @@ -1435,12 +1339,10 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], + "unplugin": ["unplugin@3.0.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg=="], "unstorage": ["unstorage@2.0.0-alpha.7", "", { "peerDependencies": { "@azure/app-configuration": "^1.11.0", "@azure/cosmos": "^4.9.1", "@azure/data-tables": "^13.3.2", "@azure/identity": "^4.13.0", "@azure/keyvault-secrets": "^4.10.0", "@azure/storage-blob": "^12.31.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.13.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.36.2", "@vercel/blob": ">=0.27.3", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "chokidar": "^4 || ^5", "db0": ">=0.3.4", "idb-keyval": "^6.2.2", "ioredis": "^5.9.3", "lru-cache": "^11.2.6", "mongodb": "^6 || ^7", "ofetch": "*", "uploadthing": "^7.7.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "chokidar", "db0", "idb-keyval", "ioredis", "lru-cache", "mongodb", "ofetch", "uploadthing"] }, "sha512-ELPztchk2zgFJnakyodVY3vJWGW9jy//keJ32IOJVGUMyaPydwcA1FtVvWqT0TNRch9H+cMNEGllfVFfScImog=="], - "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="], - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "uploadthing": ["uploadthing@7.7.4", "", { "dependencies": { "@effect/platform": "0.90.3", "@standard-schema/spec": "1.0.0-beta.4", "@uploadthing/mime-types": "0.3.6", "@uploadthing/shared": "7.1.10", "effect": "3.17.7" }, "peerDependencies": { "express": "*", "h3": "*", "tailwindcss": "^3.0.0 || ^4.0.0-beta.0" }, "optionalPeers": ["express", "h3", "tailwindcss"] }, "sha512-rlK/4JWHW5jP30syzWGBFDDXv3WJDdT8gn9OoxRJmXLoXi94hBmyyjxihGlNrKhBc81czyv8TkzMioe/OuKGfA=="], @@ -1455,11 +1357,11 @@ "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], - "vite": ["vite@8.0.12", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.0", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg=="], + "vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="], "vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="], - "vitest": ["vitest@4.1.6", "", { "dependencies": { "@vitest/expect": "4.1.6", "@vitest/mocker": "4.1.6", "@vitest/pretty-format": "4.1.6", "@vitest/runner": "4.1.6", "@vitest/snapshot": "4.1.6", "@vitest/spy": "4.1.6", "@vitest/utils": "4.1.6", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.6", "@vitest/browser-preview": "4.1.6", "@vitest/browser-webdriverio": "4.1.6", "@vitest/coverage-istanbul": "4.1.6", "@vitest/coverage-v8": "4.1.6", "@vitest/ui": "4.1.6", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ=="], + "vitest": ["vitest@4.1.9", "", { "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.9", "@vitest/browser-preview": "4.1.9", "@vitest/browser-webdriverio": "4.1.9", "@vitest/coverage-istanbul": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ=="], "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], @@ -1469,8 +1371,6 @@ "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], - "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], - "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], "whatwg-url": ["whatwg-url@15.1.0", "", { "dependencies": { "tr46": "^6.0.0", "webidl-conversions": "^8.0.0" } }, "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g=="], @@ -1479,8 +1379,6 @@ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], @@ -1493,24 +1391,22 @@ "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], - "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], - - "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "@base-ui/react/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + + "@base-ui/utils/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + + "@base-ui/utils/reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="], + "@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], @@ -1529,76 +1425,52 @@ "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], - "@tailwindcss/node/lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], + "@tailwindcss/node/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@tanstack/devtools/@tanstack/devtools-client": ["@tanstack/devtools-client@0.0.3", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.3" } }, "sha512-kl0r6N5iIL3t9gGDRAv55VRM3UIyMKVH83esRGq7xBjYsRLe/BeCIN2HqrlJkObUXQMKhy7i8ejuGOn+bDqDBw=="], - "@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@tanstack/router-generator/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@tanstack/router-plugin/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "@tanstack/router-utils/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "@tanstack/router-utils/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "@tanstack/start-plugin-core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - "@tanstack/start-plugin-core/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.40", "", {}, "sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w=="], + "@tanstack/start-plugin-core/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - "@tanstack/start-plugin-core/srvx": ["srvx@0.11.9", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-97wWJS6F0KTKAhDlHVmBzMvlBOp5FiNp3XrLoodIgYJpXxgG5tE9rX4Pg7s46n2shI4wtEsMATTS1+rI3/ubzA=="], - - "@tanstack/start-plugin-core/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "@tanstack/start-plugin-core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@types/ws/@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="], "@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "cheerio/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "data-urls/whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "eciesjs/@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], "eciesjs/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "h3/rou3": ["rou3@0.8.1", "", {}, "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA=="], "h3-v2/rou3": ["rou3@0.8.1", "", {}, "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA=="], - "h3-v2/srvx": ["srvx@0.11.9", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-97wWJS6F0KTKAhDlHVmBzMvlBOp5FiNp3XrLoodIgYJpXxgG5tE9rX4Pg7s46n2shI4wtEsMATTS1+rI3/ubzA=="], - - "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], @@ -1607,11 +1479,9 @@ "mongodb-connection-string-url/whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], - "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - - "parse5-htmlparser2-tree-adapter/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "nitro/rolldown": ["rolldown@1.0.0", "", { "dependencies": { "@oxc-project/types": "=0.129.0", "@rolldown/pluginutils": "1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0", "@rolldown/binding-darwin-arm64": "1.0.0", "@rolldown/binding-darwin-x64": "1.0.0", "@rolldown/binding-freebsd-x64": "1.0.0", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", "@rolldown/binding-linux-arm64-gnu": "1.0.0", "@rolldown/binding-linux-arm64-musl": "1.0.0", "@rolldown/binding-linux-ppc64-gnu": "1.0.0", "@rolldown/binding-linux-s390x-gnu": "1.0.0", "@rolldown/binding-linux-x64-gnu": "1.0.0", "@rolldown/binding-linux-x64-musl": "1.0.0", "@rolldown/binding-openharmony-arm64": "1.0.0", "@rolldown/binding-wasm32-wasi": "1.0.0", "@rolldown/binding-win32-arm64-msvc": "1.0.0", "@rolldown/binding-win32-x64-msvc": "1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA=="], - "parse5-parser-stream/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], @@ -1623,13 +1493,13 @@ "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0", "", {}, "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ=="], + "shadcn/postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], - "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + "shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "shadcn/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "solid-js/seroval": ["seroval@1.5.1", "", {}, "sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA=="], - "shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "solid-js/seroval-plugins": ["seroval-plugins@1.5.1", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw=="], "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -1639,21 +1509,15 @@ "tsx/esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="], + "unplugin/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "uploadthing/h3": ["h3@2.0.1-rc.16", "", { "dependencies": { "rou3": "^0.8.0", "srvx": "^0.11.9" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-h+pjvyujdo9way8qj6FUbhaQcHlR8FEq65EhTX9ViT5pK8aLj68uFl4hBkF+hsTJAH+H1END2Yv6hTIsabGfag=="], "vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "vitest/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "vitest/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], @@ -1711,41 +1575,53 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], + "@tanstack/devtools/@tanstack/devtools-client/@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.5", "", {}, "sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw=="], - "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], + "@tanstack/router-plugin/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], + "@tanstack/router-utils/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], + "mongodb-connection-string-url/whatwg-url/tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], - "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], + "mongodb-connection-string-url/whatwg-url/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], - "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], + "nitro/rolldown/@oxc-project/types": ["@oxc-project/types@0.129.0", "", {}, "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg=="], - "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], + "nitro/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA=="], - "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], + "nitro/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew=="], - "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + "nitro/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ=="], - "@tanstack/devtools/@tanstack/devtools-client/@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.5", "", {}, "sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw=="], + "nitro/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ=="], - "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "nitro/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0", "", { "os": "linux", "cpu": "arm" }, "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A=="], - "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "nitro/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ=="], - "cross-fetch/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "nitro/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA=="], - "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "nitro/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg=="], - "mongodb-connection-string-url/whatwg-url/tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + "nitro/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA=="], - "mongodb-connection-string-url/whatwg-url/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + "nitro/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA=="], + + "nitro/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw=="], + + "nitro/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0", "", { "os": "none", "cpu": "arm64" }, "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig=="], + + "nitro/rolldown/@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg=="], + + "nitro/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow=="], + + "nitro/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg=="], + + "shadcn/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="], @@ -1802,15 +1678,5 @@ "uploadthing/h3/rou3": ["rou3@0.8.1", "", {}, "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA=="], "uploadthing/h3/srvx": ["srvx@0.11.9", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-97wWJS6F0KTKAhDlHVmBzMvlBOp5FiNp3XrLoodIgYJpXxgG5tE9rX4Pg7s46n2shI4wtEsMATTS1+rI3/ubzA=="], - - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cross-fetch/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - - "cross-fetch/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], } } diff --git a/docs/decisions/0001-multi-tenancy.md b/docs/decisions/0001-multi-tenancy.md new file mode 100644 index 0000000..ec92f21 --- /dev/null +++ b/docs/decisions/0001-multi-tenancy.md @@ -0,0 +1,123 @@ +# ADR 0001: Multi-tenancy direction + +**Status:** Accepted — Option B implemented +**Date:** 2026-06-21 (accepted 2026-06-22) +**Context:** Commit series through plan 007; audit at `b44e631` + +## Context + +Clientra is mid-migration between a **single-operator workspace** (the `PRD.md` +model: one admin + their clients) and **multi-tenant agencies** (Better Auth +`organization` plugin, org-scoped columns, partial query filtering). The app is +neither cleanly single-tenant nor safely multi-tenant today. + +### Schema & auth (org infrastructure present) + +| Location | What exists | +|----------|-------------| +| `src/db/schema.ts` | `clients.organizationId`, `clientUsers.organizationId`, `session.activeOrganizationId`, full `organization` / `member` / `invitation` tables | +| `src/auth/better-auth.ts:201-227` | `organization()` plugin, `allowUserToCreateOrganization: true`, org invitation emails | +| `src/auth/roles.ts:9` | `SessionUser.activeOrganizationId` | +| `src/auth/guards.ts:25,43,71` | Admin redirects depend on `activeOrganizationId` (onboarding vs dashboard) | + +### Org-aware paths (filter or stamp `organizationId`) + +| File:line | Behavior | +|-----------|----------| +| `src/db/records.ts:904-910` | `listClientsForUser` — admin lists clients where `organizationId === activeOrganizationId` | +| `src/db/records.ts:991-998` | `listProjectsForUser` — admin projects scoped via client join on `organizationId` | +| `src/db/records.ts:1632-1640` | `searchRecords` — admin project search filtered by org | +| `src/db/records.ts:1669-1677` | `searchRecords` — admin client search filtered by org | +| `src/db/records.ts:1920-1934` | `listUsersForAdmin` — members + client users for org | +| `src/routes/api/clients.ts:38-45` | POST client requires `activeOrganizationId`, stamps `organizationId` | +| `src/routes/api/dashboard/activity.ts:21-26` | Dashboard activity requires `activeOrganizationId` | +| `src/routes/api/users.ts:22` | User list passes `activeOrganizationId` | + +### Org-blind paths (admin mutation by record id, no org check) + +These use `requireAdminMutationRequest` but do **not** verify the target record +belongs to the admin's `activeOrganizationId`: + +| Route | File | +|-------|------| +| PATCH/DELETE client | `src/routes/api/clients/$id.ts` | +| POST project | `src/routes/api/projects.ts:29-48` (no org stamp on project) | +| PATCH/DELETE project | `src/routes/api/projects/$id.ts` | +| Project updates CRUD | `src/routes/api/projects/$id/updates.ts`, `src/routes/api/project-updates/$id.ts` | +| Project milestones CRUD | `src/routes/api/projects/$id/milestones.ts`, `src/routes/api/project-milestones/$id.ts` | +| File delete | `src/routes/api/files/$id.ts` | +| Invites create/resend/revoke/approve | `src/routes/api/invites.ts`, `src/routes/api/invites/$id/*.ts` | +| Status change review | `src/routes/api/admin/status-change-requests/$id.ts` | +| User role/delete | `src/routes/api/users/$id.ts` | +| Settings | `src/routes/api/settings.ts` | + +An isolation check on each would compare the record's `organizationId` (directly +on `clients`, or via `projects.clientId → clients.organizationId`) against +`auth.user.activeOrganizationId`. + +### Intended product model + +`PRD.md` describes a **single Admin persona** managing their clients — not +enterprise multi-agency CRM. `phases.md` does not list multi-tenancy as a launch +goal. Normal operation seeds **one admin** (`seedIfEmpty`); everyone else is +`client`. There is effectively **one tenant in production today**. + +**Open question for maintainer:** Is a second independent agency workspace a +near-term product goal, or was the org plugin added for future optionality? + +## Option A — Remove or freeze multi-tenancy + +**Effort:** M (schema migration + auth simplification) + +**Actions:** + +- Remove or stop using the Better Auth `organization()` plugin and worker-invite + flow (`/invite/worker/:token`). +- Drop nullable `organizationId` columns (or leave nullable but stop writing them). +- Remove `activeOrganizationId` from session-dependent redirects; simplify + onboarding. +- Delete or archive `organization`, `member`, `invitation` tables via migration. +- Simplify `listClientsForUser` / `searchRecords` to admin-sees-all (single tenant). + +**Gains:** Less dead schema, fewer partial code paths, no false sense of isolation. + +**Blast radius:** Migration touches auth tables, seed data, guards, and every +org-filtered query. Verify session creation does not depend on an active org +(load-bearing check before deleting). + +## Option B — Finish multi-tenancy + +**Effort:** L (enforcement pass + UX + tests) + +**Actions:** + +- Add org-ownership checks to every org-blind admin route listed above. +- Stamp `organizationId` on project creation (derive from `clientId`). +- Org-switch UI for admins with multiple memberships. +- Org-scoped seeding (no hardcoded org id). +- Cross-org isolation integration tests (extend plan 005 access-control suite). +- Revisit indexes: composite `(organizationId, …)` per plan 004 maintenance note. + +**Gains:** True workspace isolation if multiple agencies are a product goal. + +**Risk:** Incomplete enforcement is worse than no enforcement — every blind path +must be closed before launch. + +## Decision + +**Option B (finish multi-tenancy)** is the implemented path. This PR closes the +org-blind admin routes enumerated above by adding org-scoped ownership guards +(`adminOwnsClient`, `adminOwnsProject`, and siblings in `src/db/records.ts`) to +every admin mutation route, stamps `organizationId` on client/project creation, +and adds cross-org isolation coverage in `src/__tests__/access-control.test.ts`. + +**Option A (freeze/remove)** remains a documented fallback if the product scope +is reconsidered post-launch and multiple agency workspaces are ruled out; it +would mean dropping the org plugin, columns, and org-scoped query filtering. + +## Consequences + +| Decision | Follow-up | +|----------|-----------| +| Option A | Simplify data layer; plan 006+ splits proceed without org scoping in every module | +| Option B | Block launch until org-blind routes are enumerated and tested; add cross-org cases to `access-control.test.ts` | \ No newline at end of file diff --git a/docs/decisions/0002-colleague-invites.md b/docs/decisions/0002-colleague-invites.md new file mode 100644 index 0000000..0d820ed --- /dev/null +++ b/docs/decisions/0002-colleague-invites.md @@ -0,0 +1,103 @@ +# ADR 0002: Client-initiated colleague invites + +**Status:** Accepted — Option A implemented +**Date:** 2026-06-21 +**Context:** Audit at `b44e631`; wiring re-verified 2026-06-21 + +## Context + +Portal clients can invite colleagues to share their client workspace. The feature +is **partially complete** with a user-facing bug in the email timing. + +### Wiring inventory + +| Step | File:line | Behavior | +|------|-----------|----------| +| Schema | `src/db/schema.ts:133-137` | `invites.initiatedByClientId`, `invites.adminApprovedAt` | +| Client creates invite | `src/routes/api/portal/team.ts:62-68` | `createPortalColleagueInvite` with `initiatedByClientId: team.clientId` | +| **Email sent immediately** | `src/routes/api/portal/team.ts:77-84` | `sendInviteEmail` on POST — before any approval | +| Portal copy (approval-gated) | `src/routes/portal/team.tsx:83,107` | UI says invite is sent *after admin approves* | +| Redeem gated | `src/db/records.ts:1756-1758` | `getActiveInviteByToken` requires `adminApprovedAt` when `initiatedByClientId` is set | +| Consume gated | `src/db/records.ts:1784-1786` | Same clause in `consumeInvite` | +| Approval API | `src/routes/api/invites/$id/approve.ts:20` | `approveInviteRecord` sets `adminApprovedAt`, re-sends email | +| **Admin approval UI** | `src/routes/clients/$id.tsx:679-725` | `InviteRow` shows "Awaiting Approval" badge + Approve button for `initiatedByClientId && !adminApprovedAt` | + +**Drift since original audit:** An admin approval surface **does exist** on the +client detail page (`PendingInvitesPanel` / `InviteRow`). The original plan +claimed it was missing; that is no longer accurate. + +### The remaining bug + +Regardless of UI completeness, **creation still emails a dead link**: + +1. Client POSTs → invite row created, **email sent immediately**. +2. Recipient clicks link → `getActiveInviteByToken` returns `null` (not approved). +3. Admin approves later → approval route **re-sends** a working email. + +Users who only read the first email get a broken experience. Portal copy correctly +describes approval-gated delivery, but the server contradicts it. + +### Security note + +Colleague invites flow through the same redeem path as admin invites. Plan 001 +forces `role: client` on Better Auth signup; no privilege-escalation path found. + +The approve route (`/api/invites/$id/approve`) is used for **all** pending invites +on a client, not colleague-only — Option B must not delete it. + +## Option A — Complete the feature + +**Effort:** M + +1. **Fix email timing** (`src/routes/api/portal/team.ts`): + - Remove `sendInviteEmail` from POST creation. + - On creation, notify admin (new lightweight notification or dashboard signal) + that approval is pending. + - Keep email send on `src/routes/api/invites/$id/approve.ts` (already re-sends). + +2. **Align resend behavior** (`src/routes/api/invites/$id/resend.ts`): + - Block or no-op resend for unapproved colleague invites (or resend admin + notification instead of client email). + +3. **Tests** (`src/__tests__/invite-lifecycle.test.ts` or new API test): + - Colleague invite: token inactive until `approveInviteRecord`, active after. + - No email on create (mock `sendInviteEmail`). + +4. **Copy** (`src/routes/portal/team.tsx`): Already correct; no change needed. + +**Files touched:** `portal/team.ts`, `invites/$id/resend.ts`, `notifications.ts`, +one test file. + +## Option B — Remove the feature + +**Effort:** S + +1. Remove POST handler branch in `src/routes/api/portal/team.ts` (keep GET team + listing). +2. Remove `createPortalColleagueInvite` from data layer. +3. Migration: drop `initiated_by_client_id`, `admin_approved_at` columns; simplify + `getActiveInviteByToken` / `consumeInvite` (remove approval `or` clause). +4. Remove approval UI branch in `src/routes/clients/$id.tsx` (`isAwaitingApproval`). +5. Update `src/routes/portal/team.tsx` — remove colleague-invite form/copy. + +**Do not remove** `approveInviteRecord` or `/api/invites/$id/approve` — used for +standard admin invites. + +**Migration blast radius:** `invites` table columns, redeem queries, portal team +UI, client detail invite row, API types in `src/lib/api.ts`. + +## Recommendation + +**Option A (complete)** with the email-timing fix only is the smallest path to a +coherent feature. The admin UI already exists; the main gap is **server behavior +matching the UI promise**. This aligns with `PRD.md` item 7 (client invites) and +the lightweight scope. + +Option B is defensible if the team wants **less surface area at launch** and is +willing to drop colleague self-service invites entirely (admin-only invites remain). + +## Immediate action (regardless of A/B) + +At minimum before launch, **stop sending the broken first email** on colleague-invite +creation — either move send to approval (Option A) or disable POST (Option B). The +current state ships contradictory copy *and* a dead link. \ No newline at end of file diff --git a/drizzle/0011_medical_scarlet_witch.sql b/drizzle/0011_medical_scarlet_witch.sql new file mode 100644 index 0000000..1cf95f4 --- /dev/null +++ b/drizzle/0011_medical_scarlet_witch.sql @@ -0,0 +1,27 @@ +CREATE TABLE `status_change_requests` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `requested_by` text NOT NULL, + `requested_status` text NOT NULL, + `reason` text NOT NULL, + `approval_state` text DEFAULT 'pending' NOT NULL, + `reviewed_by` text, + `reviewed_at` integer, + `created_at` integer NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`requested_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`reviewed_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE INDEX `status_change_requests_project_id_idx` ON `status_change_requests` (`project_id`);--> statement-breakpoint +ALTER TABLE `invites` ADD `initiated_by_client_id` text REFERENCES clients(id) ON DELETE SET NULL;--> statement-breakpoint +ALTER TABLE `invites` ADD `admin_approved_at` integer;--> statement-breakpoint +CREATE INDEX `invites_client_id_idx` ON `invites` (`client_id`);--> statement-breakpoint +CREATE INDEX `client_users_user_id_idx` ON `client_users` (`user_id`);--> statement-breakpoint +ALTER TABLE `client_users` ALTER COLUMN "organization_id" TO "organization_id" text REFERENCES organization(id) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX `clients_organization_id_idx` ON `clients` (`organization_id`);--> statement-breakpoint +ALTER TABLE `clients` ALTER COLUMN "organization_id" TO "organization_id" text REFERENCES organization(id) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX `files_project_id_idx` ON `files` (`project_id`);--> statement-breakpoint +CREATE INDEX `project_milestones_project_id_idx` ON `project_milestones` (`project_id`);--> statement-breakpoint +CREATE INDEX `project_notes_project_id_idx` ON `project_notes` (`project_id`);--> statement-breakpoint +CREATE INDEX `project_updates_project_id_idx` ON `project_updates` (`project_id`); \ No newline at end of file diff --git a/drizzle/0012_pale_mastermind.sql b/drizzle/0012_pale_mastermind.sql new file mode 100644 index 0000000..d8f3475 --- /dev/null +++ b/drizzle/0012_pale_mastermind.sql @@ -0,0 +1 @@ +CREATE INDEX `invites_client_email_idx` ON `invites` (`client_id`,`email`); \ No newline at end of file diff --git a/drizzle/meta/0011_snapshot.json b/drizzle/meta/0011_snapshot.json new file mode 100644 index 0000000..b176ff2 --- /dev/null +++ b/drizzle/meta/0011_snapshot.json @@ -0,0 +1,1499 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "68a447d0-ddbb-4461-bf2a-e82680457769", + "prevId": "fd70416d-096b-4db1-b859-72d6100f632a", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_users_id_fk": { + "name": "account_user_id_users_id_fk", + "tableFrom": "account", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "client_users": { + "name": "client_users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "client_users_client_id_user_id_unique": { + "name": "client_users_client_id_user_id_unique", + "columns": ["client_id", "user_id"], + "isUnique": true + }, + "client_users_user_id_idx": { + "name": "client_users_user_id_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "client_users_client_id_clients_id_fk": { + "name": "client_users_client_id_clients_id_fk", + "tableFrom": "client_users", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "client_users_user_id_users_id_fk": { + "name": "client_users_user_id_users_id_fk", + "tableFrom": "client_users", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "client_users_organization_id_organization_id_fk": { + "name": "client_users_organization_id_organization_id_fk", + "tableFrom": "client_users", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "clients": { + "name": "clients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company": { + "name": "company", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "clients_organization_id_idx": { + "name": "clients_organization_id_idx", + "columns": ["organization_id"], + "isUnique": false + } + }, + "foreignKeys": { + "clients_organization_id_organization_id_fk": { + "name": "clients_organization_id_organization_id_fk", + "tableFrom": "clients", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "files": { + "name": "files", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "files_storage_key_unique": { + "name": "files_storage_key_unique", + "columns": ["storage_key"], + "isUnique": true + }, + "files_project_id_idx": { + "name": "files_project_id_idx", + "columns": ["project_id"], + "isUnique": false + } + }, + "foreignKeys": { + "files_project_id_projects_id_fk": { + "name": "files_project_id_projects_id_fk", + "tableFrom": "files", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "files_uploaded_by_users_id_fk": { + "name": "files_uploaded_by_users_id_fk", + "tableFrom": "files", + "tableTo": "users", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": ["organization_id"], + "isUnique": false + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": ["email"], + "isUnique": false + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_users_id_fk": { + "name": "invitation_inviter_id_users_id_fk", + "tableFrom": "invitation", + "tableTo": "users", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invites": { + "name": "invites", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiated_by_client_id": { + "name": "initiated_by_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_approved_at": { + "name": "admin_approved_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "invites_token_unique": { + "name": "invites_token_unique", + "columns": ["token"], + "isUnique": true + }, + "invites_client_id_idx": { + "name": "invites_client_id_idx", + "columns": ["client_id"], + "isUnique": false + } + }, + "foreignKeys": { + "invites_client_id_clients_id_fk": { + "name": "invites_client_id_clients_id_fk", + "tableFrom": "invites", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invites_initiated_by_client_id_clients_id_fk": { + "name": "invites_initiated_by_client_id_clients_id_fk", + "tableFrom": "invites", + "tableTo": "clients", + "columnsFrom": ["initiated_by_client_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "member": { + "name": "member", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": ["organization_id"], + "isUnique": false + }, + "member_user_id_idx": { + "name": "member_user_id_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_users_id_fk": { + "name": "member_user_id_users_id_fk", + "tableFrom": "member", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "columns": ["slug"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_milestones": { + "name": "project_milestones", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'todo'" + }, + "due_date": { + "name": "due_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_milestones_project_id_idx": { + "name": "project_milestones_project_id_idx", + "columns": ["project_id"], + "isUnique": false + } + }, + "foreignKeys": { + "project_milestones_project_id_projects_id_fk": { + "name": "project_milestones_project_id_projects_id_fk", + "tableFrom": "project_milestones", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_notes": { + "name": "project_notes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_notes_project_id_idx": { + "name": "project_notes_project_id_idx", + "columns": ["project_id"], + "isUnique": false + } + }, + "foreignKeys": { + "project_notes_project_id_projects_id_fk": { + "name": "project_notes_project_id_projects_id_fk", + "tableFrom": "project_notes", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_notes_user_id_users_id_fk": { + "name": "project_notes_user_id_users_id_fk", + "tableFrom": "project_notes", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_updates": { + "name": "project_updates", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'on_track'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_updates_project_id_idx": { + "name": "project_updates_project_id_idx", + "columns": ["project_id"], + "isUnique": false + } + }, + "foreignKeys": { + "project_updates_project_id_projects_id_fk": { + "name": "project_updates_project_id_projects_id_fk", + "tableFrom": "project_updates", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_updates_author_id_users_id_fk": { + "name": "project_updates_author_id_users_id_fk", + "tableFrom": "project_updates", + "tableTo": "users", + "columnsFrom": ["author_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "budget": { + "name": "budget", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "deadline": { + "name": "deadline", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "projects_client_id_slug_unique": { + "name": "projects_client_id_slug_unique", + "columns": ["client_id", "slug"], + "isUnique": true + } + }, + "foreignKeys": { + "projects_client_id_clients_id_fk": { + "name": "projects_client_id_clients_id_fk", + "tableFrom": "projects", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": ["token"], + "isUnique": true + } + }, + "foreignKeys": { + "session_user_id_users_id_fk": { + "name": "session_user_id_users_id_fk", + "tableFrom": "session", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "status_change_requests": { + "name": "status_change_requests", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "requested_status": { + "name": "requested_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "approval_state": { + "name": "approval_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "status_change_requests_project_id_idx": { + "name": "status_change_requests_project_id_idx", + "columns": ["project_id"], + "isUnique": false + } + }, + "foreignKeys": { + "status_change_requests_project_id_projects_id_fk": { + "name": "status_change_requests_project_id_projects_id_fk", + "tableFrom": "status_change_requests", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_change_requests_requested_by_users_id_fk": { + "name": "status_change_requests_requested_by_users_id_fk", + "tableFrom": "status_change_requests", + "tableTo": "users", + "columnsFrom": ["requested_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_change_requests_reviewed_by_users_id_fk": { + "name": "status_change_requests_reviewed_by_users_id_fk", + "tableFrom": "status_change_requests", + "tableTo": "users", + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": ["email"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workspace_settings": { + "name": "workspace_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "workspace_name": { + "name": "workspace_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Clientra'" + }, + "support_email": { + "name": "support_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'support@clientra.com'" + }, + "allow_signups": { + "name": "allow_signups", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_notifications": { + "name": "enable_notifications", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "auto_archive": { + "name": "auto_archive", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "portal_url": { + "name": "portal_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/0012_snapshot.json b/drizzle/meta/0012_snapshot.json new file mode 100644 index 0000000..c0eb576 --- /dev/null +++ b/drizzle/meta/0012_snapshot.json @@ -0,0 +1,1504 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "76490324-1d7f-4ecd-91e0-0d0210eb622d", + "prevId": "68a447d0-ddbb-4461-bf2a-e82680457769", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_users_id_fk": { + "name": "account_user_id_users_id_fk", + "tableFrom": "account", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "client_users": { + "name": "client_users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "client_users_client_id_user_id_unique": { + "name": "client_users_client_id_user_id_unique", + "columns": ["client_id", "user_id"], + "isUnique": true + }, + "client_users_user_id_idx": { + "name": "client_users_user_id_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "client_users_client_id_clients_id_fk": { + "name": "client_users_client_id_clients_id_fk", + "tableFrom": "client_users", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "client_users_user_id_users_id_fk": { + "name": "client_users_user_id_users_id_fk", + "tableFrom": "client_users", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "client_users_organization_id_organization_id_fk": { + "name": "client_users_organization_id_organization_id_fk", + "tableFrom": "client_users", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "clients": { + "name": "clients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company": { + "name": "company", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "clients_organization_id_idx": { + "name": "clients_organization_id_idx", + "columns": ["organization_id"], + "isUnique": false + } + }, + "foreignKeys": { + "clients_organization_id_organization_id_fk": { + "name": "clients_organization_id_organization_id_fk", + "tableFrom": "clients", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "files": { + "name": "files", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "files_storage_key_unique": { + "name": "files_storage_key_unique", + "columns": ["storage_key"], + "isUnique": true + }, + "files_project_id_idx": { + "name": "files_project_id_idx", + "columns": ["project_id"], + "isUnique": false + } + }, + "foreignKeys": { + "files_project_id_projects_id_fk": { + "name": "files_project_id_projects_id_fk", + "tableFrom": "files", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "files_uploaded_by_users_id_fk": { + "name": "files_uploaded_by_users_id_fk", + "tableFrom": "files", + "tableTo": "users", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": ["organization_id"], + "isUnique": false + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": ["email"], + "isUnique": false + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_users_id_fk": { + "name": "invitation_inviter_id_users_id_fk", + "tableFrom": "invitation", + "tableTo": "users", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invites": { + "name": "invites", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiated_by_client_id": { + "name": "initiated_by_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_approved_at": { + "name": "admin_approved_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "invites_token_unique": { + "name": "invites_token_unique", + "columns": ["token"], + "isUnique": true + }, + "invites_client_id_idx": { + "name": "invites_client_id_idx", + "columns": ["client_id"], + "isUnique": false + }, + "invites_client_email_idx": { + "name": "invites_client_email_idx", + "columns": ["client_id", "email"], + "isUnique": false + } + }, + "foreignKeys": { + "invites_client_id_clients_id_fk": { + "name": "invites_client_id_clients_id_fk", + "tableFrom": "invites", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invites_initiated_by_client_id_clients_id_fk": { + "name": "invites_initiated_by_client_id_clients_id_fk", + "tableFrom": "invites", + "tableTo": "clients", + "columnsFrom": ["initiated_by_client_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "member": { + "name": "member", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": ["organization_id"], + "isUnique": false + }, + "member_user_id_idx": { + "name": "member_user_id_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_users_id_fk": { + "name": "member_user_id_users_id_fk", + "tableFrom": "member", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "columns": ["slug"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_milestones": { + "name": "project_milestones", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'todo'" + }, + "due_date": { + "name": "due_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_milestones_project_id_idx": { + "name": "project_milestones_project_id_idx", + "columns": ["project_id"], + "isUnique": false + } + }, + "foreignKeys": { + "project_milestones_project_id_projects_id_fk": { + "name": "project_milestones_project_id_projects_id_fk", + "tableFrom": "project_milestones", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_notes": { + "name": "project_notes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_notes_project_id_idx": { + "name": "project_notes_project_id_idx", + "columns": ["project_id"], + "isUnique": false + } + }, + "foreignKeys": { + "project_notes_project_id_projects_id_fk": { + "name": "project_notes_project_id_projects_id_fk", + "tableFrom": "project_notes", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_notes_user_id_users_id_fk": { + "name": "project_notes_user_id_users_id_fk", + "tableFrom": "project_notes", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_updates": { + "name": "project_updates", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'on_track'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_updates_project_id_idx": { + "name": "project_updates_project_id_idx", + "columns": ["project_id"], + "isUnique": false + } + }, + "foreignKeys": { + "project_updates_project_id_projects_id_fk": { + "name": "project_updates_project_id_projects_id_fk", + "tableFrom": "project_updates", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_updates_author_id_users_id_fk": { + "name": "project_updates_author_id_users_id_fk", + "tableFrom": "project_updates", + "tableTo": "users", + "columnsFrom": ["author_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "budget": { + "name": "budget", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "deadline": { + "name": "deadline", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "projects_client_id_slug_unique": { + "name": "projects_client_id_slug_unique", + "columns": ["client_id", "slug"], + "isUnique": true + } + }, + "foreignKeys": { + "projects_client_id_clients_id_fk": { + "name": "projects_client_id_clients_id_fk", + "tableFrom": "projects", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": ["token"], + "isUnique": true + } + }, + "foreignKeys": { + "session_user_id_users_id_fk": { + "name": "session_user_id_users_id_fk", + "tableFrom": "session", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "status_change_requests": { + "name": "status_change_requests", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "requested_status": { + "name": "requested_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "approval_state": { + "name": "approval_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "status_change_requests_project_id_idx": { + "name": "status_change_requests_project_id_idx", + "columns": ["project_id"], + "isUnique": false + } + }, + "foreignKeys": { + "status_change_requests_project_id_projects_id_fk": { + "name": "status_change_requests_project_id_projects_id_fk", + "tableFrom": "status_change_requests", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_change_requests_requested_by_users_id_fk": { + "name": "status_change_requests_requested_by_users_id_fk", + "tableFrom": "status_change_requests", + "tableTo": "users", + "columnsFrom": ["requested_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_change_requests_reviewed_by_users_id_fk": { + "name": "status_change_requests_reviewed_by_users_id_fk", + "tableFrom": "status_change_requests", + "tableTo": "users", + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": ["email"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workspace_settings": { + "name": "workspace_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "workspace_name": { + "name": "workspace_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Clientra'" + }, + "support_email": { + "name": "support_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'support@clientra.com'" + }, + "allow_signups": { + "name": "allow_signups", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_notifications": { + "name": "enable_notifications", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "auto_archive": { + "name": "auto_archive", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "portal_url": { + "name": "portal_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 7adac7a..b4f479b 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -78,6 +78,20 @@ "when": 1780669963686, "tag": "0010_naive_starfox", "breakpoints": true + }, + { + "idx": 11, + "version": "6", + "when": 1781989131744, + "tag": "0011_medical_scarlet_witch", + "breakpoints": true + }, + { + "idx": 12, + "version": "6", + "when": 1782138017724, + "tag": "0012_pale_mastermind", + "breakpoints": true } ] } diff --git a/package.json b/package.json index eeead12..9eee2d4 100644 --- a/package.json +++ b/package.json @@ -14,56 +14,56 @@ "fix": "ultracite fix", "db:studio": "drizzle-kit studio", "db:sync": "bun x drizzle-kit push && bun run src/db/seed.ts", - "doctor": "npx react-doctor@2" + "doctor": "npx react-doctor@latest" }, "dependencies": { - "@base-ui/react": "^1.3.0", - "@fontsource-variable/geist": "^5.2.8", - "@hugeicons/core-free-icons": "^4.0.0", - "@hugeicons/react": "^1.1.6", - "@libsql/client": "^0.17.0", - "@tailwindcss/vite": "^4.1.18", - "@tanstack/react-devtools": "^0.7.0", - "@tanstack/react-query": "^5", - "@tanstack/react-router": "^1.132.0", - "@tanstack/react-router-devtools": "^1.132.0", - "@tanstack/react-router-ssr-query": "^1.131.7", - "@tanstack/react-start": "^1.132.0", + "@base-ui/react": "^1.6.0", + "@fontsource-variable/geist": "^5.2.9", + "@hugeicons/core-free-icons": "^4.2.1", + "@hugeicons/react": "^1.1.7", + "@libsql/client": "^0.17.4", + "@tailwindcss/vite": "^4.3.1", + "@tanstack/react-devtools": "^0.7.11", + "@tanstack/react-query": "^5.101.0", + "@tanstack/react-router": "^1.170.16", + "@tanstack/react-router-devtools": "^1.167.0", + "@tanstack/react-router-ssr-query": "^1.167.1", + "@tanstack/react-start": "^1.168.26", "@uploadthing/react": "^7.3.3", - "better-auth": "^1.5.5", + "better-auth": "^1.6.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "drizzle-orm": "^0.45.1", + "drizzle-orm": "^0.45.2", "loops": "^6.3.0", - "motion": "^12.39.0", + "motion": "^12.40.0", "nitro": "3.0.260429-beta", - "react": "^19.2.0", - "react-dom": "^19.2.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", "recharts": "^3.8.1", - "shadcn": "^4.0.6", - "tailwind-merge": "^3.5.0", - "tailwindcss": "^4.0.6", + "shadcn": "^4.11.0", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4.3.1", "tw-animate-css": "^1.4.0", "uploadthing": "7.7.4", - "zod": "^4.3.6" + "zod": "^4.4.3" }, "overrides": { "nf3": "0.3.17" }, "devDependencies": { "@biomejs/biome": "2.4.7", - "@tanstack/devtools-vite": "^0.3.11", - "@testing-library/dom": "^10.4.0", - "@testing-library/react": "^16.2.0", - "@types/node": "^22.10.2", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", - "@vitejs/plugin-react": "^6.0.1", - "drizzle-kit": "^0.31.9", - "jsdom": "^27.0.0", - "typescript": "^5.7.2", + "@tanstack/devtools-vite": "^0.3.12", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.2", + "@types/node": "^22.20.0", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.2", + "drizzle-kit": "^0.31.10", + "jsdom": "^27.4.0", + "typescript": "^5.9.3", "ultracite": "7.3.2", - "vite": "^8.0.12", - "vitest": "^4" + "vite": "^8.0.16", + "vitest": "^4.1.9" } } diff --git a/src/__tests__/access-control.test.ts b/src/__tests__/access-control.test.ts new file mode 100644 index 0000000..c7e9098 --- /dev/null +++ b/src/__tests__/access-control.test.ts @@ -0,0 +1,332 @@ +import { readdir, readFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { createClient } from "@libsql/client"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { ROLES, type SessionUser } from "@/auth/roles"; + +async function applyMigrations(url: string) { + const client = createClient({ url }); + const migrationDirectory = path.resolve(process.cwd(), "drizzle"); + const fileNames = (await readdir(migrationDirectory)) + .filter((fileName) => fileName.endsWith(".sql")) + .sort(); + + for (const fileName of fileNames) { + const sql = await readFile(path.join(migrationDirectory, fileName), "utf8"); + const statements = sql + .split("--> statement-breakpoint") + .map((statement) => statement.trim()) + .filter(Boolean); + + for (const statement of statements) { + await client.execute(statement); + } + } + + return client; +} + +async function createRecordsTestContext() { + vi.resetModules(); + + const databasePath = path + .join( + os.tmpdir(), + `clientra-access-control-${Date.now()}-${Math.random() + .toString(36) + .slice(2)}.db` + ) + .replaceAll("\\", "/"); + const databaseUrl = `file:${databasePath}`; + + process.env.TURSO_DATABASE_URL = databaseUrl; + // biome-ignore lint/performance/noDelete: the libSQL client treats a literal "undefined" token differently from an absent token. + delete process.env.TURSO_AUTH_TOKEN; + + const client = await applyMigrations(databaseUrl); + const records = await import("@/db/records"); + + return { client, records }; +} + +async function seedOrganization( + client: ReturnType, + organizationId: string +) { + await client.execute({ + args: [ + organizationId, + `Test Org ${organizationId}`, + `test-org-${organizationId}`, + 1_741_000_000_000, + ], + sql: `insert into organization (id, name, slug, created_at) + values (?, ?, ?, ?)`, + }); +} + +async function seedAccessControlScenario( + client: ReturnType +) { + const timestamp = 1_741_000_000_000; + + await seedOrganization(client, "org_1"); + await seedOrganization(client, "org_2"); + + const users = [ + ["admin_1", "Admin User", "admin@example.com", ROLES.ADMIN], + ["client_a", "Client A User", "clienta@example.com", ROLES.CLIENT], + ["client_b", "Client B User", "clientb@example.com", ROLES.CLIENT], + ] as const; + + for (const [id, name, email, role] of users) { + await client.execute({ + args: [id, name, email, 1, null, role, timestamp, timestamp], + sql: `insert into users + (id, name, email, email_verified, image, role, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?, ?)`, + }); + } + + await client.execute({ + args: [ + "client_a_record", + "Client A Contact", + "Client A", + "a@clienta.com", + "", + "", + "active", + "", + "[]", + timestamp, + ], + sql: `insert into clients + (id, name, company, email, phone, website, status, notes, tags, created_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + }); + + await client.execute({ + args: [ + "client_b_record", + "Client B Contact", + "Client B", + "b@clientb.com", + "", + "", + "active", + "", + "[]", + timestamp, + ], + sql: `insert into clients + (id, name, company, email, phone, website, status, notes, tags, created_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + }); + + await client.execute({ + args: ["link_a", "client_a_record", "client_a"], + sql: "insert into client_users (id, client_id, user_id) values (?, ?, ?)", + }); + + await client.execute({ + args: ["member_admin", "org_1", "admin_1", "owner", timestamp], + sql: `insert into member (id, organization_id, user_id, role, created_at) + values (?, ?, ?, ?, ?)`, + }); + + await client.execute({ + args: ["org_1", "client_a_record"], + sql: "update clients set organization_id = ? where id = ?", + }); + + await client.execute({ + args: ["org_2", "client_b_record"], + sql: "update clients set organization_id = ? where id = ?", + }); + + await client.execute({ + args: [ + "project_a", + "client_a_record", + "Client A Project", + "client-a-project", + "in_progress", + 10_000, + "2026-04-10", + "Project under Client A.", + timestamp, + ], + sql: `insert into projects + (id, client_id, title, slug, status, budget, deadline, description, created_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + }); + + await client.execute({ + args: [ + "file_a", + "project_a", + "client_a", + "storage-key-a", + "https://example.com/a.pdf", + "a.pdf", + 1024, + "application/pdf", + timestamp, + ], + sql: `insert into files + (id, project_id, uploaded_by, storage_key, file_url, file_name, file_size, mime_type, created_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + }); + + await client.execute({ + args: [ + "note_a", + "project_a", + "client_a", + "Comment from linked client.", + timestamp, + ], + sql: `insert into project_notes + (id, project_id, user_id, content, created_at) + values (?, ?, ?, ?, ?)`, + }); +} + +describe("access control", () => { + const clientsToClose: ReturnType[] = []; + + afterEach(async () => { + while (clientsToClose.length > 0) { + const client = clientsToClose.pop(); + + if (client) { + await client.close(); + } + } + }); + + it("enforces project access for admins, linked clients, and outsiders", async () => { + const { client, records } = await createRecordsTestContext(); + clientsToClose.push(client); + await seedAccessControlScenario(client); + + const admin: SessionUser = { + activeOrganizationId: "org_1", + email: "admin@example.com", + id: "admin_1", + name: "Admin User", + role: ROLES.ADMIN, + }; + const outsideAdmin: SessionUser = { + activeOrganizationId: "org_2", + email: "outside-admin@example.com", + id: "admin_2", + name: "Outside Admin", + role: ROLES.ADMIN, + }; + const clientA: SessionUser = { + email: "clienta@example.com", + id: "client_a", + name: "Client A User", + role: ROLES.CLIENT, + }; + const clientB: SessionUser = { + email: "clientb@example.com", + id: "client_b", + name: "Client B User", + role: ROLES.CLIENT, + }; + + expect(await records.canAccessProject(admin, "project_a")).toBe(true); + expect(await records.canAccessProject(outsideAdmin, "project_a")).toBe( + false + ); + expect( + await records.canAccessProject( + { ...admin, activeOrganizationId: null }, + "project_a" + ) + ).toBe(false); + expect(await records.canAccessProject(clientA, "project_a")).toBe(true); + expect(await records.canAccessProject(clientB, "project_a")).toBe(false); + expect(await records.canAccessProject(clientA, "missing_project")).toBe( + false + ); + }, 15_000); + + it("scopes admin client mutations to the active organization", async () => { + const { client, records } = await createRecordsTestContext(); + clientsToClose.push(client); + await seedAccessControlScenario(client); + + const admin: SessionUser = { + activeOrganizationId: "org_1", + email: "admin@example.com", + id: "admin_1", + name: "Admin User", + role: ROLES.ADMIN, + }; + const outsideAdmin: SessionUser = { + activeOrganizationId: "org_2", + email: "outside-admin@example.com", + id: "admin_2", + name: "Outside Admin", + role: ROLES.ADMIN, + }; + + expect(await records.adminOwnsClient(admin, "client_a_record")).toBe(true); + expect(await records.adminOwnsClient(admin, "client_b_record")).toBe(false); + expect(await records.adminOwnsClient(outsideAdmin, "client_a_record")).toBe( + false + ); + expect(await records.adminOwnsProject(admin, "project_a")).toBe(true); + expect(await records.adminOwnsProject(outsideAdmin, "project_a")).toBe( + false + ); + expect(await records.adminManagesUser(admin, "admin_1")).toBe(true); + expect(await records.adminManagesUser(admin, "client_a")).toBe(true); + expect(await records.adminManagesUser(admin, "client_b")).toBe(false); + expect(await records.adminManagesUser(outsideAdmin, "client_a")).toBe( + false + ); + }, 15_000); + + it("returns null from list helpers when access is denied", async () => { + const { client, records } = await createRecordsTestContext(); + clientsToClose.push(client); + await seedAccessControlScenario(client); + + const clientA: SessionUser = { + email: "clienta@example.com", + id: "client_a", + name: "Client A User", + role: ROLES.CLIENT, + }; + const clientB: SessionUser = { + email: "clientb@example.com", + id: "client_b", + name: "Client B User", + role: ROLES.CLIENT, + }; + + expect( + await records.listProjectFilesForUser("project_a", clientB) + ).toBeNull(); + expect( + await records.listProjectCommentsForUser("project_a", clientB) + ).toBeNull(); + + const files = await records.listProjectFilesForUser("project_a", clientA); + const comments = await records.listProjectCommentsForUser( + "project_a", + clientA + ); + + expect(files).not.toBeNull(); + expect(files?.length).toBeGreaterThan(0); + expect(comments).not.toBeNull(); + expect(comments?.length).toBeGreaterThan(0); + }, 15_000); +}); diff --git a/src/__tests__/api-admin-crud.test.ts b/src/__tests__/api-admin-crud.test.ts index c2d37fc..33f8514 100644 --- a/src/__tests__/api-admin-crud.test.ts +++ b/src/__tests__/api-admin-crud.test.ts @@ -12,6 +12,8 @@ vi.mock("@/auth/session.server", () => ({ })); vi.mock("@/db/records", () => ({ + adminOwnsClient: vi.fn(), + adminOwnsProject: vi.fn(), createProjectRecord: vi.fn(), deleteClientRecord: vi.fn(), deleteProjectRecord: vi.fn(), @@ -24,13 +26,14 @@ vi.mock("@/db/records", () => ({ listClientStorageKeys: vi.fn(), listProjectsForUser: vi.fn(), listProjectStorageKeys: vi.fn(), - seedIfEmpty: vi.fn(), updateClientRecord: vi.fn(), updateProjectRecord: vi.fn(), })); import { getSessionUserFromHeaders } from "@/auth/session.server"; import { + adminOwnsClient, + adminOwnsProject, createProjectRecord, DuplicateProjectSlugError, deleteClientRecord, @@ -38,7 +41,6 @@ import { listClientStorageKeys, listProjectStorageKeys, listProjectsForUser, - seedIfEmpty, updateClientRecord, updateProjectRecord, } from "@/db/records"; @@ -62,6 +64,7 @@ const projectsHandlers = ProjectsRoute.options.server?.handlers as { }; const adminUser = { + activeOrganizationId: "org_1", email: "admin@example.com", id: "admin_1", name: "Admin User", @@ -103,10 +106,11 @@ const validProjectPayload = { describe("admin CRUD API routes", () => { beforeEach(() => { vi.clearAllMocks(); + vi.mocked(adminOwnsClient).mockResolvedValue(true); + vi.mocked(adminOwnsProject).mockResolvedValue(true); vi.mocked(listClientStorageKeys).mockResolvedValue([]); vi.mocked(listProjectStorageKeys).mockResolvedValue([]); vi.mocked(listProjectsForUser).mockResolvedValue([]); - vi.mocked(seedIfEmpty).mockResolvedValue(undefined); }); it("rejects unauthenticated client updates", async () => { @@ -196,6 +200,22 @@ describe("admin CRUD API routes", () => { expect(updateClientRecord).not.toHaveBeenCalled(); }); + it("rejects client updates outside the active organization", async () => { + vi.mocked(getSessionUserFromHeaders).mockResolvedValue(adminUser); + vi.mocked(adminOwnsClient).mockResolvedValue(false); + + const response = await clientHandlers.PATCH({ + params: { id: "client_other_org" }, + request: createRequest("/api/clients/client_other_org", { + body: JSON.stringify(validClientPayload), + method: "PATCH", + }), + } as never); + + expect(response.status).toBe(404); + expect(updateClientRecord).not.toHaveBeenCalled(); + }); + it("updates a client for an admin", async () => { vi.mocked(getSessionUserFromHeaders).mockResolvedValue(adminUser); vi.mocked(updateClientRecord).mockResolvedValue({ @@ -237,6 +257,21 @@ describe("admin CRUD API routes", () => { expect(response.status).toBe(404); }); + it("rejects client deletes outside the active organization", async () => { + vi.mocked(getSessionUserFromHeaders).mockResolvedValue(adminUser); + vi.mocked(adminOwnsClient).mockResolvedValue(false); + + const response = await clientHandlers.DELETE({ + params: { id: "client_other_org" }, + request: createRequest("/api/clients/client_other_org", { + method: "DELETE", + }), + } as never); + + expect(response.status).toBe(404); + expect(deleteClientRecord).not.toHaveBeenCalled(); + }); + it("deletes a client for an admin", async () => { vi.mocked(getSessionUserFromHeaders).mockResolvedValue(adminUser); vi.mocked(deleteClientRecord).mockResolvedValue(true); @@ -253,6 +288,22 @@ describe("admin CRUD API routes", () => { await expect(response.json()).resolves.toEqual({ success: true }); }); + it("rejects project updates outside the active organization", async () => { + vi.mocked(getSessionUserFromHeaders).mockResolvedValue(adminUser); + vi.mocked(adminOwnsProject).mockResolvedValue(false); + + const response = await projectHandlers.PATCH({ + params: { id: "project_other_org" }, + request: createRequest("/api/projects/project_other_org", { + body: JSON.stringify(validProjectPayload), + method: "PATCH", + }), + } as never); + + expect(response.status).toBe(404); + expect(updateProjectRecord).not.toHaveBeenCalled(); + }); + it("updates a project for an admin", async () => { vi.mocked(getSessionUserFromHeaders).mockResolvedValue(adminUser); vi.mocked(updateProjectRecord).mockResolvedValue({ @@ -280,8 +331,24 @@ describe("admin CRUD API routes", () => { }); }); + it("rejects project creation outside the active organization", async () => { + vi.mocked(getSessionUserFromHeaders).mockResolvedValue(adminUser); + vi.mocked(adminOwnsClient).mockResolvedValue(false); + + const response = await projectsHandlers.POST({ + request: createRequest("/api/projects", { + body: JSON.stringify(validProjectPayload), + method: "POST", + }), + } as never); + + expect(response.status).toBe(404); + expect(createProjectRecord).not.toHaveBeenCalled(); + }); + it("returns 409 when creating a duplicate project name under a client", async () => { vi.mocked(getSessionUserFromHeaders).mockResolvedValue(adminUser); + vi.mocked(adminOwnsClient).mockResolvedValue(true); vi.mocked(createProjectRecord).mockRejectedValue( new DuplicateProjectSlugError() ); @@ -333,6 +400,21 @@ describe("admin CRUD API routes", () => { expect(response.status).toBe(404); }); + it("rejects project deletes outside the active organization", async () => { + vi.mocked(getSessionUserFromHeaders).mockResolvedValue(adminUser); + vi.mocked(adminOwnsProject).mockResolvedValue(false); + + const response = await projectHandlers.DELETE({ + params: { id: "project_other_org" }, + request: createRequest("/api/projects/project_other_org", { + method: "DELETE", + }), + } as never); + + expect(response.status).toBe(404); + expect(deleteProjectRecord).not.toHaveBeenCalled(); + }); + it("deletes a project for an admin", async () => { vi.mocked(getSessionUserFromHeaders).mockResolvedValue(adminUser); vi.mocked(deleteProjectRecord).mockResolvedValue(true); diff --git a/src/__tests__/api-collaboration.test.ts b/src/__tests__/api-collaboration.test.ts index 67e8bb7..2ac69bb 100644 --- a/src/__tests__/api-collaboration.test.ts +++ b/src/__tests__/api-collaboration.test.ts @@ -10,7 +10,6 @@ vi.mock("@/db/records", () => ({ createProjectNoteRecord: vi.fn(), getProjectCollaboration: vi.fn(), getProjectNotificationContext: vi.fn(), - seedIfEmpty: vi.fn(), serializeProjectComment: vi.fn((comment) => comment), })); @@ -25,7 +24,6 @@ import { createProjectNoteRecord, getProjectCollaboration, getProjectNotificationContext, - seedIfEmpty, serializeProjectComment, } from "@/db/records"; import { Route as NotesRoute } from "@/routes/api/notes"; @@ -85,7 +83,6 @@ describe("collaboration API routes", () => { ), } as never); - expect(seedIfEmpty).toHaveBeenCalled(); expect(response.status).toBe(200); await expect(response.json()).resolves.toEqual({ activity: [ diff --git a/src/__tests__/api-invite-management.test.ts b/src/__tests__/api-invite-management.test.ts index 95c5901..2095797 100644 --- a/src/__tests__/api-invite-management.test.ts +++ b/src/__tests__/api-invite-management.test.ts @@ -6,6 +6,7 @@ vi.mock("@/auth/session.server", () => ({ })); vi.mock("@/db/records", () => ({ + adminOwnsClient: vi.fn(), createInviteRecord: vi.fn(), getActiveInviteById: vi.fn(), getClientById: vi.fn(), @@ -21,6 +22,7 @@ vi.mock("@/server/email/notifications", () => ({ import { getSessionUserFromHeaders } from "@/auth/session.server"; import { + adminOwnsClient, approveInviteRecord, createInviteRecord, getActiveInviteById, @@ -49,6 +51,7 @@ const approveHandlers = ApproveInviteRoute.options.server?.handlers as { }; const adminUser = { + activeOrganizationId: "org_1", email: "admin@example.com", id: "admin_1", name: "Admin User", @@ -100,9 +103,28 @@ describe("invite management API routes", () => { vi.mocked(refreshInviteExpiration).mockResolvedValue(invite); vi.mocked(revokeInviteRecord).mockResolvedValue(invite); vi.mocked(approveInviteRecord).mockResolvedValue(invite); + vi.mocked(adminOwnsClient).mockResolvedValue(true); + vi.mocked(getInviteRecordById).mockResolvedValue(invite); vi.mocked(sendInviteEmail).mockResolvedValue({ skipped: false }); }); + it("rejects invite creation outside the active organization", async () => { + vi.mocked(adminOwnsClient).mockResolvedValue(false); + + const response = await inviteHandlers.POST({ + request: createRequest("/api/invites", { + body: JSON.stringify({ + clientId: "client_other_org", + email: "jordan@example.com", + }), + method: "POST", + }), + } as never); + + expect(response.status).toBe(404); + expect(createInviteRecord).not.toHaveBeenCalled(); + }); + it("blocks invite creation when Loop invite email fails", async () => { vi.mocked(sendInviteEmail).mockRejectedValue(new Error("Loop failed")); @@ -122,6 +144,40 @@ describe("invite management API routes", () => { expect(revokeInviteRecord).toHaveBeenCalledWith("invite_1"); }); + it("blocks resend for unapproved colleague invites", async () => { + vi.mocked(getActiveInviteById).mockResolvedValue({ + ...invite, + initiatedByClientId: "client_1", + adminApprovedAt: null, + }); + + const response = await resendHandlers.POST({ + params: { id: "invite_1" }, + request: createRequest("/api/invites/invite_1/resend", { + method: "POST", + }), + } as never); + + expect(response.status).toBe(409); + expect(sendInviteEmail).not.toHaveBeenCalled(); + expect(refreshInviteExpiration).not.toHaveBeenCalled(); + }); + + it("does not send resend email when invite refresh fails", async () => { + vi.mocked(refreshInviteExpiration).mockResolvedValue(null); + + const response = await resendHandlers.POST({ + params: { id: "invite_1" }, + request: createRequest("/api/invites/invite_1/resend", { + method: "POST", + }), + } as never); + + expect(response.status).toBe(500); + expect(refreshInviteExpiration).toHaveBeenCalled(); + expect(sendInviteEmail).not.toHaveBeenCalled(); + }); + it("resends an active invite and blocks when Loop fails", async () => { vi.mocked(sendInviteEmail).mockRejectedValue(new Error("Loop failed")); @@ -133,8 +189,8 @@ describe("invite management API routes", () => { } as never); expect(response.status).toBe(500); + expect(refreshInviteExpiration).toHaveBeenCalled(); expect(sendInviteEmail).toHaveBeenCalled(); - expect(refreshInviteExpiration).not.toHaveBeenCalled(); }); it("revokes active pending invites", async () => { @@ -204,6 +260,24 @@ describe("invite management API routes", () => { expect(approveInviteRecord).not.toHaveBeenCalled(); }); + it("rejects re-approving an already approved invite", async () => { + vi.mocked(getInviteRecordById).mockResolvedValue({ + ...invite, + adminApprovedAt: new Date("2026-06-06T10:00:00.000Z"), + }); + + const response = await approveHandlers.POST({ + params: { id: "invite_1" }, + request: createRequest("/api/invites/invite_1/approve", { + method: "POST", + }), + } as never); + + expect(response.status).toBe(409); + expect(approveInviteRecord).not.toHaveBeenCalled(); + expect(sendInviteEmail).not.toHaveBeenCalled(); + }); + it("approves client-initiated colleague invites", async () => { const approvedInvite = { ...invite, diff --git a/src/__tests__/api-pending-invites.test.ts b/src/__tests__/api-pending-invites.test.ts index e1d12ed..9862c4b 100644 --- a/src/__tests__/api-pending-invites.test.ts +++ b/src/__tests__/api-pending-invites.test.ts @@ -6,11 +6,12 @@ vi.mock("@/auth/session.server", () => ({ })); vi.mock("@/db/records", () => ({ + adminOwnsClient: vi.fn(), listPendingInvitesForClient: vi.fn(), })); import { getSessionUserFromHeaders } from "@/auth/session.server"; -import { listPendingInvitesForClient } from "@/db/records"; +import { adminOwnsClient, listPendingInvitesForClient } from "@/db/records"; import { Route as PendingInvitesRoute } from "@/routes/api/clients/$id/invites"; const pendingInvitesHandlers = PendingInvitesRoute.options.server?.handlers as { @@ -24,6 +25,7 @@ function createRequest(path: string) { describe("pending invite API route", () => { beforeEach(() => { vi.clearAllMocks(); + vi.mocked(adminOwnsClient).mockResolvedValue(true); vi.mocked(listPendingInvitesForClient).mockResolvedValue([]); }); @@ -56,6 +58,25 @@ describe("pending invite API route", () => { expect(listPendingInvitesForClient).not.toHaveBeenCalled(); }); + it("rejects pending invite reads outside the active organization", async () => { + vi.mocked(getSessionUserFromHeaders).mockResolvedValue({ + activeOrganizationId: "org_1", + email: "admin@example.com", + id: "admin_1", + name: "Admin User", + role: ROLES.ADMIN, + }); + vi.mocked(adminOwnsClient).mockResolvedValue(false); + + const response = await pendingInvitesHandlers.GET({ + params: { id: "client_other_org" }, + request: createRequest("/api/clients/client_other_org/invites"), + } as never); + + expect(response.status).toBe(404); + expect(listPendingInvitesForClient).not.toHaveBeenCalled(); + }); + it("returns sanitized pending invites for admins", async () => { vi.mocked(getSessionUserFromHeaders).mockResolvedValue({ email: "admin@example.com", diff --git a/src/__tests__/api-portal-summary.test.ts b/src/__tests__/api-portal-summary.test.ts new file mode 100644 index 0000000..e953005 --- /dev/null +++ b/src/__tests__/api-portal-summary.test.ts @@ -0,0 +1,64 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ROLES } from "@/auth/roles"; + +vi.mock("@/auth/session.server", () => ({ + getSessionUserFromHeaders: vi.fn(), +})); + +vi.mock("@/db/records", () => ({ + getPortalSummary: vi.fn(), +})); + +import { getSessionUserFromHeaders } from "@/auth/session.server"; +import { getPortalSummary } from "@/db/records"; +import { Route as PortalSummaryRoute } from "@/routes/api/portal/summary"; + +const handlers = PortalSummaryRoute.options.server?.handlers as { + GET: (context: unknown) => Promise; +}; + +describe("portal summary API route", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("rejects admin callers before loading portal summary data", async () => { + vi.mocked(getSessionUserFromHeaders).mockResolvedValue({ + activeOrganizationId: "org_1", + email: "admin@example.com", + id: "admin_1", + name: "Admin User", + role: ROLES.ADMIN, + }); + + const response = await handlers.GET({ + request: new Request("https://clientra.test/api/portal/summary"), + } as never); + + expect(response.status).toBe(403); + expect(getPortalSummary).not.toHaveBeenCalled(); + }); + + it("returns portal summary for linked client users", async () => { + vi.mocked(getSessionUserFromHeaders).mockResolvedValue({ + email: "client@example.com", + id: "client_user", + name: "Client User", + role: ROLES.CLIENT, + }); + vi.mocked(getPortalSummary).mockResolvedValue({ + activeProjects: [], + latestUpdates: [], + projectCount: 0, + recentFiles: [], + upcomingMilestones: [], + }); + + const response = await handlers.GET({ + request: new Request("https://clientra.test/api/portal/summary"), + } as never); + + expect(response.status).toBe(200); + expect(getPortalSummary).toHaveBeenCalled(); + }); +}); diff --git a/src/__tests__/api-portal-team.test.ts b/src/__tests__/api-portal-team.test.ts new file mode 100644 index 0000000..63cdae7 --- /dev/null +++ b/src/__tests__/api-portal-team.test.ts @@ -0,0 +1,126 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ROLES } from "@/auth/roles"; + +vi.mock("@/auth/session.server", () => ({ + getSessionUserFromHeaders: vi.fn(), +})); + +vi.mock("@/db/records", () => ({ + createPortalColleagueInvite: vi.fn(), + listPortalTeam: vi.fn(), +})); + +vi.mock("@/server/email/notifications", () => ({ + sendInviteEmail: vi.fn(), +})); + +import { getSessionUserFromHeaders } from "@/auth/session.server"; +import { createPortalColleagueInvite, listPortalTeam } from "@/db/records"; +import { Route as PortalTeamRoute } from "@/routes/api/portal/team"; +import { sendInviteEmail } from "@/server/email/notifications"; + +const portalTeamHandlers = PortalTeamRoute.options.server?.handlers as { + POST: (context: unknown) => Promise; +}; + +const clientUser = { + email: "client@example.com", + id: "client_user", + name: "Client User", + role: ROLES.CLIENT, +}; + +const createdInvite = { + adminApprovedAt: null, + clientId: "client_1", + consumedAt: null, + createdAt: new Date("2026-06-06T10:00:00.000Z"), + email: "colleague@example.com", + expiresAt: new Date("2026-06-13T10:00:00.000Z"), + id: "invite_colleague", + initiatedByClientId: "client_1", + revokedAt: null, + token: "secret-token", +}; + +function createRequest(path: string, init?: RequestInit) { + return new Request(`https://clientra.test${path}`, { + headers: { + "content-type": "application/json", + origin: "https://clientra.test", + "sec-fetch-site": "same-origin", + ...init?.headers, + }, + ...init, + }); +} + +describe("portal team API routes", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(getSessionUserFromHeaders).mockResolvedValue(clientUser); + vi.mocked(listPortalTeam).mockResolvedValue({ + clientId: "client_1", + members: [], + pendingInvites: [], + }); + vi.mocked(createPortalColleagueInvite).mockImplementation( + async (input) => ({ + ...createdInvite, + email: input.email, + id: input.id, + }) + ); + }); + + it("creates colleague invites without sending email before approval", async () => { + const response = await portalTeamHandlers.POST({ + request: createRequest("/api/portal/team", { + body: JSON.stringify({ + email: "colleague@example.com", + }), + method: "POST", + }), + } as never); + + expect(response.status).toBe(201); + expect(createPortalColleagueInvite).toHaveBeenCalledWith( + expect.objectContaining({ + clientId: "client_1", + email: "colleague@example.com", + initiatedByClientId: "client_1", + }) + ); + expect(sendInviteEmail).not.toHaveBeenCalled(); + const body = await response.json(); + expect(body).toMatchObject({ + adminApprovedAt: null, + clientId: "client_1", + email: "colleague@example.com", + initiatedByClientId: "client_1", + }); + expect(body.id).toEqual(expect.any(String)); + expect(body).not.toHaveProperty("token"); + }); + + it("returns 200 without leaking the token for duplicate pending invites", async () => { + vi.mocked(createPortalColleagueInvite).mockResolvedValue({ + ...createdInvite, + id: "invite_existing", + }); + + const response = await portalTeamHandlers.POST({ + request: createRequest("/api/portal/team", { + body: JSON.stringify({ + email: "colleague@example.com", + }), + method: "POST", + }), + } as never); + + expect(response.status).toBe(200); + const body = await response.json(); + expect(body).not.toHaveProperty("token"); + expect(body.id).toBe("invite_existing"); + }); +}); diff --git a/src/__tests__/api-project-milestones.test.ts b/src/__tests__/api-project-milestones.test.ts index 3ede1d0..aa37bf2 100644 --- a/src/__tests__/api-project-milestones.test.ts +++ b/src/__tests__/api-project-milestones.test.ts @@ -6,10 +6,11 @@ vi.mock("@/auth/session.server", () => ({ })); vi.mock("@/db/records", () => ({ + adminOwnsProject: vi.fn(), + adminOwnsProjectMilestone: vi.fn(), canAccessProject: vi.fn(), createProjectMilestoneRecord: vi.fn(), deleteProjectMilestoneRecord: vi.fn(), - getProjectById: vi.fn(), listProjectMilestonesForUser: vi.fn(), serializeProjectMilestone: vi.fn((milestone) => ({ ...milestone, @@ -27,10 +28,10 @@ vi.mock("@/db/records", () => ({ import { getSessionUserFromHeaders } from "@/auth/session.server"; import { - canAccessProject, + adminOwnsProject, + adminOwnsProjectMilestone, createProjectMilestoneRecord, deleteProjectMilestoneRecord, - getProjectById, listProjectMilestonesForUser, updateProjectMilestoneRecord, } from "@/db/records"; @@ -48,6 +49,7 @@ const milestoneHandlers = ProjectMilestoneRoute.options.server?.handlers as { }; const adminUser = { + activeOrganizationId: "org_1", email: "admin@example.com", id: "admin_1", name: "Admin User", @@ -84,6 +86,8 @@ function createRequest(path: string, init?: RequestInit) { describe("project milestone API routes", () => { beforeEach(() => { vi.clearAllMocks(); + vi.mocked(adminOwnsProject).mockResolvedValue(true); + vi.mocked(adminOwnsProjectMilestone).mockResolvedValue(true); }); it("lets authorized users read milestones", async () => { @@ -134,18 +138,6 @@ describe("project milestone API routes", () => { it("lets admins create milestones", async () => { vi.mocked(getSessionUserFromHeaders).mockResolvedValue(adminUser); - vi.mocked(getProjectById).mockResolvedValue({ - budget: 12_000, - clientId: "client_1", - createdAt: new Date("2026-04-01T10:00:00.000Z"), - deadline: "2026-04-30", - description: "Delivery portal.", - id: "project_1", - slug: "delivery-portal", - status: "in_progress", - title: "Delivery Portal", - }); - vi.mocked(canAccessProject).mockResolvedValue(true); vi.mocked(createProjectMilestoneRecord).mockResolvedValue({ createdAt: new Date("2026-04-01T10:00:00.000Z"), description: validPayload.description, diff --git a/src/__tests__/api-project-updates.test.ts b/src/__tests__/api-project-updates.test.ts index 7e08e90..daaaa25 100644 --- a/src/__tests__/api-project-updates.test.ts +++ b/src/__tests__/api-project-updates.test.ts @@ -6,10 +6,11 @@ vi.mock("@/auth/session.server", () => ({ })); vi.mock("@/db/records", () => ({ + adminOwnsProject: vi.fn(), + adminOwnsProjectUpdate: vi.fn(), canAccessProject: vi.fn(), createProjectUpdateRecord: vi.fn(), deleteProjectUpdateRecord: vi.fn(), - getProjectById: vi.fn(), getProjectNotificationContext: vi.fn(), listProjectUpdatesForUser: vi.fn(), serializeProjectUpdate: vi.fn((update) => ({ @@ -33,10 +34,10 @@ vi.mock("@/server/email/notifications", () => ({ import { getSessionUserFromHeaders } from "@/auth/session.server"; import { - canAccessProject, + adminOwnsProject, + adminOwnsProjectUpdate, createProjectUpdateRecord, deleteProjectUpdateRecord, - getProjectById, getProjectNotificationContext, listProjectUpdatesForUser, updateProjectUpdateRecord, @@ -56,6 +57,7 @@ const updateHandlers = ProjectUpdateRoute.options.server?.handlers as { }; const adminUser = { + activeOrganizationId: "org_1", email: "admin@example.com", id: "admin_1", name: "Admin User", @@ -90,6 +92,8 @@ function createRequest(path: string, init?: RequestInit) { describe("project update API routes", () => { beforeEach(() => { vi.clearAllMocks(); + vi.mocked(adminOwnsProject).mockResolvedValue(true); + vi.mocked(adminOwnsProjectUpdate).mockResolvedValue(true); vi.mocked(getProjectNotificationContext).mockResolvedValue({ clientCompany: "Acme", clientName: "Jordan", @@ -154,18 +158,6 @@ describe("project update API routes", () => { it("lets admins publish project updates", async () => { vi.mocked(getSessionUserFromHeaders).mockResolvedValue(adminUser); - vi.mocked(getProjectById).mockResolvedValue({ - budget: 12_000, - clientId: "client_1", - createdAt: new Date("2026-04-01T10:00:00.000Z"), - deadline: "2026-04-30", - description: "Delivery portal.", - id: "project_1", - slug: "delivery-portal", - status: "in_progress", - title: "Delivery Portal", - }); - vi.mocked(canAccessProject).mockResolvedValue(true); vi.mocked(createProjectUpdateRecord).mockResolvedValue({ authorId: "admin_1", authorName: "Admin User", @@ -199,18 +191,6 @@ describe("project update API routes", () => { it("keeps project update creation successful when notifications fail", async () => { vi.mocked(getSessionUserFromHeaders).mockResolvedValue(adminUser); - vi.mocked(canAccessProject).mockResolvedValue(true); - vi.mocked(getProjectById).mockResolvedValue({ - budget: 12_000, - clientId: "client_1", - createdAt: new Date("2026-04-01T10:00:00.000Z"), - deadline: "2026-04-30", - description: "Delivery portal.", - id: "project_1", - slug: "delivery-portal", - status: "in_progress", - title: "Delivery Portal", - }); vi.mocked(createProjectUpdateRecord).mockResolvedValue({ authorId: "admin_1", authorName: "Admin User", @@ -236,6 +216,21 @@ describe("project update API routes", () => { expect(notifyProjectUpdate).toHaveBeenCalled(); }); + it("rejects project update mutations outside the active organization", async () => { + vi.mocked(getSessionUserFromHeaders).mockResolvedValue(adminUser); + vi.mocked(adminOwnsProjectUpdate).mockResolvedValue(false); + + const response = await updateHandlers.DELETE({ + params: { id: "update_other_org" }, + request: createRequest("/api/project-updates/update_other_org", { + method: "DELETE", + }), + } as never); + + expect(response.status).toBe(404); + expect(deleteProjectUpdateRecord).not.toHaveBeenCalled(); + }); + it("validates update payloads and missing records", async () => { vi.mocked(getSessionUserFromHeaders).mockResolvedValue(adminUser); diff --git a/src/__tests__/auth-role-hardening.test.ts b/src/__tests__/auth-role-hardening.test.ts new file mode 100644 index 0000000..784fa5b --- /dev/null +++ b/src/__tests__/auth-role-hardening.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; +import { userAdditionalFields } from "@/auth/better-auth"; +import { ROLES } from "@/auth/roles"; + +describe("auth role hardening", () => { + it("does not accept role as user input at signup", () => { + expect(userAdditionalFields.role.input).toBe(false); + }); + + it("defaults new accounts to the client role", () => { + expect(userAdditionalFields.role.defaultValue).toBe(ROLES.CLIENT); + }); +}); diff --git a/src/__tests__/invite-lifecycle.test.ts b/src/__tests__/invite-lifecycle.test.ts new file mode 100644 index 0000000..7eb5124 --- /dev/null +++ b/src/__tests__/invite-lifecycle.test.ts @@ -0,0 +1,278 @@ +import { readdir, readFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { createClient } from "@libsql/client"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { ROLES } from "@/auth/roles"; + +async function applyMigrations(url: string) { + const client = createClient({ url }); + const migrationDirectory = path.resolve(process.cwd(), "drizzle"); + const fileNames = (await readdir(migrationDirectory)) + .filter((fileName) => fileName.endsWith(".sql")) + .sort(); + + for (const fileName of fileNames) { + const sql = await readFile(path.join(migrationDirectory, fileName), "utf8"); + const statements = sql + .split("--> statement-breakpoint") + .map((statement) => statement.trim()) + .filter(Boolean); + + for (const statement of statements) { + await client.execute(statement); + } + } + + return client; +} + +async function createRecordsTestContext() { + vi.resetModules(); + + const databasePath = path + .join( + os.tmpdir(), + `clientra-invite-lifecycle-${Date.now()}-${Math.random() + .toString(36) + .slice(2)}.db` + ) + .replaceAll("\\", "/"); + const databaseUrl = `file:${databasePath}`; + + process.env.TURSO_DATABASE_URL = databaseUrl; + // biome-ignore lint/performance/noDelete: the libSQL client treats a literal "undefined" token differently from an absent token. + delete process.env.TURSO_AUTH_TOKEN; + + const client = await applyMigrations(databaseUrl); + const records = await import("@/db/records"); + + return { client, records }; +} + +async function seedInviteClient(client: ReturnType) { + const timestamp = 1_741_000_000_000; + + await client.execute({ + args: [ + "invite_client", + "Invite Client", + "Invite Co", + "invite@example.com", + "", + "", + "active", + "", + "[]", + timestamp, + ], + sql: `insert into clients + (id, name, company, email, phone, website, status, notes, tags, created_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + }); + + await client.execute({ + args: [ + "redeem_user", + "Redeem User", + "redeem@example.com", + 1, + null, + ROLES.ADMIN, + timestamp, + timestamp, + ], + sql: `insert into users + (id, name, email, email_verified, image, role, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?, ?)`, + }); +} + +async function insertInvite( + client: ReturnType, + input: { + adminApprovedAt?: number | null; + consumedAt?: number | null; + expiresAt: number; + id: string; + initiatedByClientId?: string | null; + revokedAt?: number | null; + token: string; + } +) { + const createdAt = 1_741_000_000_000; + + await client.execute({ + args: [ + input.id, + "invite_client", + "redeem@example.com", + input.token, + input.expiresAt, + input.consumedAt ?? null, + input.revokedAt ?? null, + createdAt, + input.initiatedByClientId ?? null, + input.adminApprovedAt ?? null, + ], + sql: `insert into invites + (id, client_id, email, token, expires_at, consumed_at, revoked_at, created_at, initiated_by_client_id, admin_approved_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + }); +} + +describe("invite lifecycle", () => { + const clientsToClose: ReturnType[] = []; + + afterEach(async () => { + while (clientsToClose.length > 0) { + const client = clientsToClose.pop(); + + if (client) { + await client.close(); + } + } + }); + + it("returns active invites only when token is valid and unused", async () => { + const { client, records } = await createRecordsTestContext(); + clientsToClose.push(client); + await seedInviteClient(client); + + const future = 4_102_444_800_000; + const past = 1_000_000; + const now = 1_741_000_000_000; + + await insertInvite(client, { + id: "invite_fresh", + token: "token-fresh", + expiresAt: future, + }); + await insertInvite(client, { + id: "invite_expired", + token: "token-expired", + expiresAt: past, + }); + await insertInvite(client, { + id: "invite_revoked", + token: "token-revoked", + expiresAt: future, + revokedAt: now, + }); + await insertInvite(client, { + id: "invite_consumed", + token: "token-consumed", + expiresAt: future, + consumedAt: now, + }); + + expect(await records.getActiveInviteByToken("token-fresh")).not.toBeNull(); + expect(await records.getActiveInviteByToken("token-expired")).toBeNull(); + expect(await records.getActiveInviteByToken("token-revoked")).toBeNull(); + expect(await records.getActiveInviteByToken("token-consumed")).toBeNull(); + }, 15_000); + + it("consumes invites exactly once", async () => { + const { client, records } = await createRecordsTestContext(); + clientsToClose.push(client); + await seedInviteClient(client); + + const token = "token-single-use"; + + await insertInvite(client, { + id: "invite_single", + token, + expiresAt: 4_102_444_800_000, + }); + + expect(await records.consumeInvite(token)).toBe(true); + expect(await records.consumeInvite(token)).toBe(false); + expect(await records.getActiveInviteByToken(token)).toBeNull(); + }, 15_000); + + it("reuses an active pending colleague invite for the same email", async () => { + const { client, records } = await createRecordsTestContext(); + clientsToClose.push(client); + await seedInviteClient(client); + + const future = 4_102_444_800_000; + + await insertInvite(client, { + id: "invite_existing", + token: "token-existing", + expiresAt: future, + initiatedByClientId: "invite_client", + }); + + const duplicate = await records.createPortalColleagueInvite({ + clientId: "invite_client", + email: "redeem@example.com", + id: "invite_new", + initiatedByClientId: "invite_client", + token: "token-new", + }); + + expect(duplicate?.id).toBe("invite_existing"); + + const inviteRows = await client.execute( + "select count(*) as count from invites where client_id = 'invite_client' and email = 'redeem@example.com'" + ); + expect(inviteRows.rows[0]?.count).toBe(1); + }, 15_000); + + it("keeps colleague invites inactive until admin approval", async () => { + const { client, records } = await createRecordsTestContext(); + clientsToClose.push(client); + await seedInviteClient(client); + + const future = 4_102_444_800_000; + const token = "token-colleague"; + + await insertInvite(client, { + id: "invite_colleague", + token, + expiresAt: future, + initiatedByClientId: "invite_client", + }); + + expect(await records.getActiveInviteByToken(token)).toBeNull(); + expect(await records.consumeInvite(token)).toBe(false); + + const approved = await records.approveInviteRecord("invite_colleague"); + expect(approved).not.toBeNull(); + + expect(await records.getActiveInviteByToken(token)).not.toBeNull(); + expect(await records.consumeInvite(token)).toBe(true); + }, 15_000); + + it("rejects linking users to missing clients", async () => { + const { client, records } = await createRecordsTestContext(); + clientsToClose.push(client); + await seedInviteClient(client); + + await expect( + records.linkUserToClient("missing_client", "redeem_user") + ).rejects.toThrow("Client not found"); + }, 15_000); + + it("updates user role and links users to clients idempotently", async () => { + const { client, records } = await createRecordsTestContext(); + clientsToClose.push(client); + await seedInviteClient(client); + + await records.updateUserRole("redeem_user", ROLES.CLIENT); + + const roleRows = await client.execute( + "select role from users where id = 'redeem_user'" + ); + expect(roleRows.rows[0]?.role).toBe(ROLES.CLIENT); + + await records.linkUserToClient("invite_client", "redeem_user"); + await records.linkUserToClient("invite_client", "redeem_user"); + + const linkRows = await client.execute( + "select count(*) as count from client_users where client_id = 'invite_client' and user_id = 'redeem_user'" + ); + expect(linkRows.rows[0]?.count).toBe(1); + }, 15_000); +}); diff --git a/src/__tests__/records-collaboration.test.ts b/src/__tests__/records-collaboration.test.ts index 86807e5..0db0a85 100644 --- a/src/__tests__/records-collaboration.test.ts +++ b/src/__tests__/records-collaboration.test.ts @@ -50,6 +50,17 @@ async function createRecordsTestContext() { return { client, records }; } +async function seedOrganization( + client: ReturnType, + organizationId: string +) { + await client.execute({ + args: [organizationId, "Test Org", "test-org", 1_741_000_000_000], + sql: `insert into organization (id, name, slug, created_at) + values (?, ?, ?, ?)`, + }); +} + async function seedCollaborationScenario( client: ReturnType ) { @@ -111,6 +122,8 @@ async function seedCollaborationScenario( values (?, ?, ?, ?, ?, ?, ?, ?)`, }); + await seedOrganization(client, "org_1"); + await client.execute({ args: [ "client_1", @@ -129,6 +142,11 @@ async function seedCollaborationScenario( values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, }); + await client.execute({ + args: ["org_1", "client_1"], + sql: "update clients set organization_id = ? where id = ?", + }); + await client.execute({ args: ["link_1", "client_1", "client_user_1"], sql: "insert into client_users (id, client_id, user_id) values (?, ?, ?)", @@ -436,6 +454,7 @@ describe("records collaboration helpers", () => { await seedCollaborationScenario(client); const admin: SessionUser = { + activeOrganizationId: "org_1", email: "admin@example.com", id: "admin_1", name: "Admin User", diff --git a/src/auth/better-auth.ts b/src/auth/better-auth.ts index 3071702..4b3def7 100644 --- a/src/auth/better-auth.ts +++ b/src/auth/better-auth.ts @@ -3,6 +3,7 @@ import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { lastLoginMethod, organization } from "better-auth/plugins"; import { tanstackStartCookies } from "better-auth/tanstack-start"; +import { ROLES } from "@/auth/roles"; import { db } from "@/db/client"; import { loadEnvFiles } from "@/db/load-env"; import { @@ -22,6 +23,10 @@ const rawBaseUrl = process.env.BETTER_AUTH_URL?.trim(); if (!rawBaseUrl && process.env.NODE_ENV === "production") { throw new Error("Missing REQUIRED environment variable: BETTER_AUTH_URL"); } +const rawAuthSecret = process.env.BETTER_AUTH_SECRET?.trim(); +if (!rawAuthSecret && process.env.NODE_ENV === "production") { + throw new Error("Missing REQUIRED environment variable: BETTER_AUTH_SECRET"); +} const validatedBaseUrl = rawBaseUrl || "http://localhost:3000"; function normalizeCredential(value: string | undefined) { @@ -105,6 +110,14 @@ const googleProvider = getSocialProviderConfig({ provider: "google", }); +export const userAdditionalFields = { + role: { + type: "string", + defaultValue: ROLES.CLIENT, + input: false, + }, +} as const; + export const auth = betterAuth({ account: { accountLinking: { @@ -116,6 +129,16 @@ export const auth = betterAuth({ useSecureCookies: process.env.NODE_ENV === "production", }, baseURL: validatedBaseUrl, + databaseHooks: { + user: { + create: { + before: (user) => + Promise.resolve({ + data: { ...user, role: ROLES.CLIENT }, + }), + }, + }, + }, database: drizzleAdapter(db, { provider: "sqlite", schema: { @@ -172,12 +195,7 @@ export const auth = betterAuth({ emailVerified: "emailVerified", image: "image", }, - additionalFields: { - role: { - type: "string", - defaultValue: "client", - }, - }, + additionalFields: userAdditionalFields, }, plugins: [ organization({ diff --git a/src/auth/guards.ts b/src/auth/guards.ts index d054834..61fb312 100644 --- a/src/auth/guards.ts +++ b/src/auth/guards.ts @@ -68,12 +68,11 @@ export async function redirectAuthenticatedUser() { return null; } + const adminDestination = user.activeOrganizationId + ? "/dashboard" + : "/onboarding"; + throw redirect({ - to: - user.role === ROLES.ADMIN - ? user.activeOrganizationId - ? "/dashboard" - : "/onboarding" - : "/portal", + to: user.role === ROLES.ADMIN ? adminDestination : "/portal", }); } diff --git a/src/components/auth/worker-invite-form.tsx b/src/components/auth/worker-invite-form.tsx index fc4662b..fd96db7 100644 --- a/src/components/auth/worker-invite-form.tsx +++ b/src/components/auth/worker-invite-form.tsx @@ -22,6 +22,10 @@ interface InvitationData { role: string; } +interface SessionUserSummary { + email: string; +} + const handleSignOut = async () => { await authClient.signOut(); window.location.reload(); @@ -33,7 +37,9 @@ export function WorkerInviteForm({ token }: { token: string }) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); - const [sessionUser, setSessionUser] = useState(null); + const [sessionUser, setSessionUser] = useState( + null + ); // Fetch invitation details and current session useEffect(() => { @@ -51,13 +57,13 @@ export function WorkerInviteForm({ token }: { token: string }) { invRes.error.message ?? "This invitation is invalid or has expired." ); } else if (invRes.data) { - setInvitation(invRes.data as any); + setInvitation(invRes.data as InvitationData); } if (sessRes.data?.user) { - setSessionUser(sessRes.data.user); + setSessionUser({ email: sessRes.data.user.email }); } - } catch (err) { + } catch { setError("Failed to load invitation details."); } finally { setLoading(false); @@ -90,6 +96,129 @@ export function WorkerInviteForm({ token }: { token: string }) { } }; + const renderSessionActions = (invite: InvitationData) => { + if (!sessionUser) { + return ( +
+
+ Please log in or create a new account using{" "} + {invite.email} to accept this + invitation. +
+
+ + Sign in + + + Sign up + +
+
+ ); + } + + const emailsMatch = + sessionUser.email.toLowerCase() === invite.email.toLowerCase(); + + if (emailsMatch) { + return ( +
+ +
+ Logged in as {sessionUser.email} +
+
+ ); + } + + return ( +
+
+ You are currently logged in as{" "} + {sessionUser.email}, but this + invitation was sent to{" "} + {invite.email}. +
+
+ + + Go to Dashboard + +
+
+ ); + }; + + const renderCardContent = () => { + if (error) { + return ( +
+
+ {error} +
+ +
+ ); + } + + if (!invitation) { + return null; + } + + return ( +
+
+
+ Workspace +
+
+ {invitation.organizationName} +
+
+ Role:{" "} + + {invitation.role} + +
+
+ Invited Email:{" "} + + {invitation.email} + +
+
+ + {renderSessionActions(invitation)} +
+ ); + }; + if (loading) { return ( - - {error ? ( -
-
- {error} -
- -
- ) : invitation ? ( -
-
-
- Workspace -
-
- {invitation.organizationName} -
-
- Role:{" "} - - {invitation.role} - -
-
- Invited Email:{" "} - - {invitation.email} - -
-
- - {sessionUser ? ( - sessionUser.email.toLowerCase() === - invitation.email.toLowerCase() ? ( -
- -
- Logged in as {sessionUser.email} -
-
- ) : ( -
-
- You are currently logged in as{" "} - {sessionUser.email} - , but this invitation was sent to{" "} - {invitation.email}. -
-
- - - Go to Dashboard - -
-
- ) - ) : ( -
-
- Please log in or create a new account using{" "} - {invitation.email} to - accept this invitation. -
-
- - Sign in - - - Sign up - -
-
- )} -
- ) : null} -
+ {renderCardContent()}
); diff --git a/src/components/common/product-charts.tsx b/src/components/common/product-charts.tsx index 01cba76..e3d5524 100644 --- a/src/components/common/product-charts.tsx +++ b/src/components/common/product-charts.tsx @@ -60,17 +60,17 @@ export function ProjectStatusPieChart({ dark: ["#a1a1aa"], }; - if (/progress/.test(statusKey)) { + if (statusKey.includes("progress")) { colors = { light: ["#0d9488"], dark: ["#2dd4bf"], }; - } else if (/completed/.test(statusKey)) { + } else if (statusKey.includes("completed")) { colors = { light: ["#15803d"], dark: ["#22c55e"], }; - } else if (/planning/.test(statusKey)) { + } else if (statusKey.includes("planning")) { colors = { light: ["#0284c7"], dark: ["#38bdf8"], diff --git a/src/db/client.ts b/src/db/client.ts index c149473..bd3051b 100644 --- a/src/db/client.ts +++ b/src/db/client.ts @@ -1,4 +1,3 @@ -import { createClient } from "@libsql/client"; import { drizzle } from "drizzle-orm/libsql"; import { loadEnvFiles } from "./load-env"; import { @@ -18,11 +17,6 @@ import { loadEnvFiles(); -const turso = createClient({ - url: process.env.TURSO_DATABASE_URL || "file:local.db", - authToken: process.env.TURSO_AUTH_TOKEN, -}); - const schema = { account: accounts, users, @@ -38,4 +32,10 @@ const schema = { verification: verifications, }; -export const db = drizzle(turso, { schema }); +export const db = drizzle({ + connection: { + url: process.env.TURSO_DATABASE_URL || "file:local.db", + authToken: process.env.TURSO_AUTH_TOKEN, + }, + schema, +}); diff --git a/src/db/invites.ts b/src/db/invites.ts new file mode 100644 index 0000000..503edd2 --- /dev/null +++ b/src/db/invites.ts @@ -0,0 +1,343 @@ +import { and, desc, eq, gt, isNotNull, isNull, or } from "drizzle-orm"; +import type { SessionUser } from "@/auth/roles"; +import { db } from "./client"; +import { + clients as clientsTable, + clientUsers as clientUsersTable, + invites as invitesTable, + users as usersTable, +} from "./schema"; + +type DatabaseExecutor = Pick< + typeof db, + "delete" | "insert" | "select" | "transaction" | "update" +>; + +interface InviteInsert { + clientId: string; + email: string; + expiresAt: Date; + id: string; + token: string; +} + +function mapInvite(row: typeof invitesTable.$inferSelect) { + return { + adminApprovedAt: row.adminApprovedAt, + clientId: row.clientId, + consumedAt: row.consumedAt, + createdAt: row.createdAt, + email: row.email, + expiresAt: row.expiresAt, + id: row.id, + initiatedByClientId: row.initiatedByClientId, + revokedAt: row.revokedAt, + token: row.token, + }; +} + +export async function createInviteRecord(input: InviteInsert) { + await db.insert(invitesTable).values({ + clientId: input.clientId, + consumedAt: null, + createdAt: new Date(), + email: input.email, + expiresAt: input.expiresAt, + id: input.id, + revokedAt: null, + token: input.token, + }); +} + +export async function getInviteRecordById(id: string) { + const [created] = await db + .select() + .from(invitesTable) + .where(eq(invitesTable.id, id)) + .limit(1); + + return created ? mapInvite(created) : null; +} + +export async function getActiveInviteByToken(token: string) { + const [invite] = await db + .select() + .from(invitesTable) + .where( + and( + eq(invitesTable.token, token), + isNull(invitesTable.consumedAt), + isNull(invitesTable.revokedAt), + gt(invitesTable.expiresAt, new Date()), + or( + isNull(invitesTable.initiatedByClientId), + isNotNull(invitesTable.adminApprovedAt) + ) + ) + ) + .limit(1); + + if (!invite) { + return null; + } + + return mapInvite(invite); +} + +export async function consumeInvite( + token: string, + executor: DatabaseExecutor = db +) { + const consumedInvites = await executor + .update(invitesTable) + .set({ consumedAt: new Date() }) + .where( + and( + eq(invitesTable.token, token), + isNull(invitesTable.consumedAt), + isNull(invitesTable.revokedAt), + gt(invitesTable.expiresAt, new Date()), + or( + isNull(invitesTable.initiatedByClientId), + isNotNull(invitesTable.adminApprovedAt) + ) + ) + ) + .returning({ token: invitesTable.token }); + + return consumedInvites.length > 0; +} + +export async function getActiveInviteById(inviteId: string) { + const [invite] = await db + .select() + .from(invitesTable) + .where( + and( + eq(invitesTable.id, inviteId), + isNull(invitesTable.consumedAt), + isNull(invitesTable.revokedAt), + gt(invitesTable.expiresAt, new Date()) + ) + ) + .limit(1); + + return invite ? mapInvite(invite) : null; +} + +export async function refreshInviteExpiration( + inviteId: string, + expiresAt: Date +) { + const [invite] = await db + .update(invitesTable) + .set({ expiresAt }) + .where( + and( + eq(invitesTable.id, inviteId), + isNull(invitesTable.consumedAt), + isNull(invitesTable.revokedAt), + gt(invitesTable.expiresAt, new Date()) + ) + ) + .returning(); + + return invite ? mapInvite(invite) : null; +} + +export async function approveInviteRecord(inviteId: string) { + const [invite] = await db + .update(invitesTable) + .set({ adminApprovedAt: new Date() }) + .where( + and( + eq(invitesTable.id, inviteId), + isNull(invitesTable.consumedAt), + isNull(invitesTable.revokedAt), + isNull(invitesTable.adminApprovedAt), + gt(invitesTable.expiresAt, new Date()) + ) + ) + .returning(); + + return invite ? mapInvite(invite) : null; +} + +export async function revokeInviteRecord(inviteId: string) { + const [invite] = await db + .update(invitesTable) + .set({ revokedAt: new Date() }) + .where( + and( + eq(invitesTable.id, inviteId), + isNull(invitesTable.consumedAt), + isNull(invitesTable.revokedAt), + gt(invitesTable.expiresAt, new Date()) + ) + ) + .returning(); + + return invite ? mapInvite(invite) : null; +} + +export async function linkUserToClient( + clientId: string, + userId: string, + executor: DatabaseExecutor = db +) { + const [client] = await executor + .select({ organizationId: clientsTable.organizationId }) + .from(clientsTable) + .where(eq(clientsTable.id, clientId)) + .limit(1); + + if (!client) { + throw new Error("Client not found"); + } + + await executor + .insert(clientUsersTable) + .values({ + clientId, + id: crypto.randomUUID(), + organizationId: client.organizationId, + userId, + }) + .onConflictDoNothing(); +} + +export async function listPendingInvitesForClient(clientId: string) { + const rows = await db + .select() + .from(invitesTable) + .where( + and( + eq(invitesTable.clientId, clientId), + isNull(invitesTable.consumedAt), + isNull(invitesTable.revokedAt), + gt(invitesTable.expiresAt, new Date()) + ) + ) + .orderBy(desc(invitesTable.createdAt)); + + return rows.map(mapInvite); +} + +async function findActivePendingInviteForClientEmail( + executor: DatabaseExecutor, + clientId: string, + email: string +) { + const [invite] = await executor + .select() + .from(invitesTable) + .where( + and( + eq(invitesTable.clientId, clientId), + eq(invitesTable.email, email), + isNull(invitesTable.consumedAt), + isNull(invitesTable.revokedAt), + gt(invitesTable.expiresAt, new Date()) + ) + ) + .limit(1); + + return invite ? mapInvite(invite) : null; +} + +export function createPortalColleagueInvite(input: { + clientId: string; + email: string; + id: string; + initiatedByClientId: string; + token: string; +}) { + // Run the dedupe check and insert in one transaction so concurrent requests + // for the same client/email cannot both pass the check and create duplicate + // active invites. + return db.transaction(async (tx) => { + const existingInvite = await findActivePendingInviteForClientEmail( + tx, + input.clientId, + input.email + ); + + if (existingInvite) { + return existingInvite; + } + + await tx.insert(invitesTable).values({ + clientId: input.clientId, + createdAt: new Date(), + email: input.email, + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), + id: input.id, + initiatedByClientId: input.initiatedByClientId, + token: input.token, + }); + + const [created] = await tx + .select() + .from(invitesTable) + .where(eq(invitesTable.id, input.id)) + .limit(1); + + return created ? mapInvite(created) : null; + }); +} + +export async function listPortalTeam(user: SessionUser) { + const [clientUser] = await db + .select({ client: clientsTable }) + .from(clientUsersTable) + .innerJoin(clientsTable, eq(clientUsersTable.clientId, clientsTable.id)) + .where(eq(clientUsersTable.userId, user.id)) + .limit(1); + + if (!clientUser) { + return { clientId: null, members: [], pendingInvites: [] }; + } + + const clientId = clientUser.client.id; + + const [members, pendingInvites] = await Promise.all([ + db + .select({ user: usersTable }) + .from(clientUsersTable) + .innerJoin(usersTable, eq(clientUsersTable.userId, usersTable.id)) + .where(eq(clientUsersTable.clientId, clientId)), + db + .select() + .from(invitesTable) + .where( + and( + eq(invitesTable.clientId, clientId), + isNull(invitesTable.consumedAt), + isNull(invitesTable.revokedAt), + gt(invitesTable.expiresAt, new Date()) + ) + ) + .orderBy(desc(invitesTable.createdAt)), + ]); + + return { + clientId, + members: members.map(({ user }) => ({ + createdAt: user.createdAt.toISOString(), + email: user.email, + id: user.id, + image: user.image, + name: user.name, + role: user.role, + })), + pendingInvites: pendingInvites.map((invite) => ({ + adminApprovedAt: invite.adminApprovedAt?.toISOString() ?? null, + clientId: invite.clientId, + createdAt: invite.createdAt.toISOString(), + email: invite.email, + expiresAt: invite.expiresAt.toISOString(), + id: invite.id, + initiatedByClientId: invite.initiatedByClientId, + })), + }; +} diff --git a/src/db/records.ts b/src/db/records.ts index 8adc415..d25bc5d 100644 --- a/src/db/records.ts +++ b/src/db/records.ts @@ -1,15 +1,4 @@ -import { - and, - desc, - eq, - gt, - inArray, - isNotNull, - isNull, - ne, - or, - sql, -} from "drizzle-orm"; +import { and, desc, eq, inArray, ne, or, sql } from "drizzle-orm"; import { ROLES, type Role, type SessionUser } from "@/auth/roles"; import { getClientPathParam } from "@/lib/client-slugs"; import { getProjectSlug } from "@/lib/project-slugs"; @@ -24,7 +13,6 @@ import { clients as clientsTable, clientUsers as clientUsersTable, files as filesTable, - invites as invitesTable, members as membersTable, organizations as organizationsTable, projectMilestones as projectMilestonesTable, @@ -117,14 +105,6 @@ interface ProjectFileInsert { uploadedBy: string; } -interface InviteInsert { - clientId: string; - email: string; - expiresAt: Date; - id: string; - token: string; -} - interface ManagedUser { createdAt: Date; email: string; @@ -336,21 +316,6 @@ function mapProject(row: typeof projectsTable.$inferSelect) { }; } -function mapInvite(row: typeof invitesTable.$inferSelect) { - return { - clientId: row.clientId, - consumedAt: row.consumedAt, - createdAt: row.createdAt, - email: row.email, - expiresAt: row.expiresAt, - id: row.id, - revokedAt: row.revokedAt, - token: row.token, - initiatedByClientId: row.initiatedByClientId, - adminApprovedAt: row.adminApprovedAt, - }; -} - function mapManagedUser( row: typeof usersTable.$inferSelect, providers: string[] @@ -486,7 +451,7 @@ export function serializeProjectMilestone( return { createdAt: milestone.createdAt.toISOString(), description: milestone.description, - dueDate: milestone.dueDate, + dueDate: milestone.dueDate ?? "", id: milestone.id, projectId: milestone.projectId, sortOrder: milestone.sortOrder, @@ -756,19 +721,6 @@ function mapPortalProject({ } async function listPortalProjectsForUser(user: SessionUser) { - if (user.role === ROLES.ADMIN) { - const rows = await db - .select({ - client: clientsTable, - project: projectsTable, - }) - .from(projectsTable) - .innerJoin(clientsTable, eq(projectsTable.clientId, clientsTable.id)) - .orderBy(desc(projectsTable.createdAt)); - - return rows.map(mapPortalProject); - } - const rows = await db .select({ client: clientsTable, @@ -1399,7 +1351,23 @@ export async function createProjectFileRecord(input: ProjectFileInsert) { export async function canAccessProject(user: SessionUser, projectId: string) { if (user.role === ROLES.ADMIN) { - return true; + if (!user.activeOrganizationId) { + return false; + } + + const [match] = await db + .select({ projectId: projectsTable.id }) + .from(projectsTable) + .innerJoin(clientsTable, eq(projectsTable.clientId, clientsTable.id)) + .where( + and( + eq(projectsTable.id, projectId), + eq(clientsTable.organizationId, user.activeOrganizationId) + ) + ) + .limit(1); + + return Boolean(match); } const [match] = await db @@ -1626,6 +1594,7 @@ export async function restoreProjectFileRecord(file: { export async function searchRecords(query: string, user: SessionUser) { const normalized = `%${escapeLikePattern(query.toLowerCase())}%`; + // biome-ignore lint/suspicious/noExplicitAny: Drizzle row accumulator; precise typing deferred to plan 006 (records.ts split) let matchedProjectsRows: any[] = []; if (user.role === ROLES.ADMIN) { if (user.activeOrganizationId) { @@ -1709,19 +1678,6 @@ export async function searchRecords(query: string, user: SessionUser) { }; } -export async function createInviteRecord(input: InviteInsert) { - await db.insert(invitesTable).values({ - clientId: input.clientId, - consumedAt: null, - createdAt: new Date(), - email: input.email, - expiresAt: input.expiresAt, - id: input.id, - revokedAt: null, - token: input.token, - }); -} - export async function getClientById(clientId: string) { const [client] = await db .select() @@ -1732,156 +1688,105 @@ export async function getClientById(clientId: string) { return client ? mapClient(client) : null; } -export async function getInviteRecordById(id: string) { - const [created] = await db - .select() - .from(invitesTable) - .where(eq(invitesTable.id, id)) +export async function adminOwnsClient(user: SessionUser, clientId: string) { + if (user.role !== ROLES.ADMIN || !user.activeOrganizationId) { + return false; + } + + const [client] = await db + .select({ organizationId: clientsTable.organizationId }) + .from(clientsTable) + .where(eq(clientsTable.id, clientId)) .limit(1); - return created ? mapInvite(created) : null; + return client?.organizationId === user.activeOrganizationId; } -export async function getActiveInviteByToken(token: string) { - const [invite] = await db - .select() - .from(invitesTable) - .where( - and( - eq(invitesTable.token, token), - isNull(invitesTable.consumedAt), - isNull(invitesTable.revokedAt), - gt(invitesTable.expiresAt, new Date()), - or( - isNull(invitesTable.initiatedByClientId), - isNotNull(invitesTable.adminApprovedAt) - ) - ) - ) - .limit(1); - - if (!invite) { - return null; +export async function adminOwnsProject(user: SessionUser, projectId: string) { + if (user.role !== ROLES.ADMIN || !user.activeOrganizationId) { + return false; } - return mapInvite(invite); + const [project] = await db + .select({ organizationId: clientsTable.organizationId }) + .from(projectsTable) + .innerJoin(clientsTable, eq(projectsTable.clientId, clientsTable.id)) + .where(eq(projectsTable.id, projectId)) + .limit(1); + + return project?.organizationId === user.activeOrganizationId; } -export async function consumeInvite( - token: string, - executor: DatabaseExecutor = db +export async function adminOwnsProjectUpdate( + user: SessionUser, + updateId: string ) { - const consumedInvites = await executor - .update(invitesTable) - .set({ consumedAt: new Date() }) - .where( - and( - eq(invitesTable.token, token), - isNull(invitesTable.consumedAt), - isNull(invitesTable.revokedAt), - gt(invitesTable.expiresAt, new Date()), - or( - isNull(invitesTable.initiatedByClientId), - isNotNull(invitesTable.adminApprovedAt) - ) - ) - ) - .returning({ token: invitesTable.token }); + const [update] = await db + .select({ projectId: projectUpdatesTable.projectId }) + .from(projectUpdatesTable) + .where(eq(projectUpdatesTable.id, updateId)) + .limit(1); + + if (!update) { + return false; + } - return consumedInvites.length > 0; + return adminOwnsProject(user, update.projectId); } -export async function getActiveInviteById(inviteId: string) { - const [invite] = await db - .select() - .from(invitesTable) - .where( - and( - eq(invitesTable.id, inviteId), - isNull(invitesTable.consumedAt), - isNull(invitesTable.revokedAt), - gt(invitesTable.expiresAt, new Date()) - ) - ) +export async function adminOwnsProjectMilestone( + user: SessionUser, + milestoneId: string +) { + const [milestone] = await db + .select({ projectId: projectMilestonesTable.projectId }) + .from(projectMilestonesTable) + .where(eq(projectMilestonesTable.id, milestoneId)) .limit(1); - return invite ? mapInvite(invite) : null; + if (!milestone) { + return false; + } + + return adminOwnsProject(user, milestone.projectId); } -export async function refreshInviteExpiration( - inviteId: string, - expiresAt: Date +export async function adminManagesUser( + user: SessionUser, + targetUserId: string ) { - const [invite] = await db - .update(invitesTable) - .set({ expiresAt }) - .where( - and( - eq(invitesTable.id, inviteId), - isNull(invitesTable.consumedAt), - isNull(invitesTable.revokedAt), - gt(invitesTable.expiresAt, new Date()) - ) - ) - .returning(); - - return invite ? mapInvite(invite) : null; -} + if (user.role !== ROLES.ADMIN || !user.activeOrganizationId) { + return false; + } -export async function approveInviteRecord(inviteId: string) { - const [invite] = await db - .update(invitesTable) - .set({ adminApprovedAt: new Date() }) + const [member] = await db + .select({ userId: membersTable.userId }) + .from(membersTable) .where( and( - eq(invitesTable.id, inviteId), - isNull(invitesTable.consumedAt), - isNull(invitesTable.revokedAt), - gt(invitesTable.expiresAt, new Date()) + eq(membersTable.organizationId, user.activeOrganizationId), + eq(membersTable.userId, targetUserId) ) ) - .returning(); + .limit(1); - return invite ? mapInvite(invite) : null; -} + if (member) { + return true; + } -export async function revokeInviteRecord(inviteId: string) { - const [invite] = await db - .update(invitesTable) - .set({ revokedAt: new Date() }) + const [clientUser] = await db + .select({ userId: clientUsersTable.userId }) + .from(clientUsersTable) + .innerJoin(clientsTable, eq(clientUsersTable.clientId, clientsTable.id)) .where( and( - eq(invitesTable.id, inviteId), - isNull(invitesTable.consumedAt), - isNull(invitesTable.revokedAt), - gt(invitesTable.expiresAt, new Date()) + eq(clientsTable.organizationId, user.activeOrganizationId), + eq(clientUsersTable.userId, targetUserId) ) ) - .returning(); - - return invite ? mapInvite(invite) : null; -} - -export async function linkUserToClient( - clientId: string, - userId: string, - executor: DatabaseExecutor = db -) { - const [client] = await executor - .select({ organizationId: clientsTable.organizationId }) - .from(clientsTable) - .where(eq(clientsTable.id, clientId)) .limit(1); - await executor - .insert(clientUsersTable) - .values({ - clientId, - id: crypto.randomUUID(), - userId, - organizationId: client?.organizationId ?? null, - }) - .onConflictDoNothing(); + return Boolean(clientUser); } export async function updateUserRole( @@ -1946,6 +1851,7 @@ export async function listUsersForAdmin(orgId: string) { ); const userIds = Array.from(userMap.keys()); + // biome-ignore lint/suspicious/noExplicitAny: Drizzle row accumulator; precise typing deferred to plan 006 (records.ts split) let accountRows: any[] = []; if (userIds.length > 0) { accountRows = await db @@ -2018,24 +1924,13 @@ export function deleteUserById(userId: string) { }); } -export async function listPendingInvitesForClient(clientId: string) { - const rows = await db - .select() - .from(invitesTable) - .where( - and( - eq(invitesTable.clientId, clientId), - isNull(invitesTable.consumedAt), - isNull(invitesTable.revokedAt), - gt(invitesTable.expiresAt, new Date()) - ) - ) - .orderBy(desc(invitesTable.createdAt)); - - return rows.map(mapInvite); -} - export async function seedIfEmpty() { + const databaseUrl = process.env.TURSO_DATABASE_URL; + const isLocalDatabase = !databaseUrl || databaseUrl.startsWith("file:"); + if (!isLocalDatabase) { + return; + } + const targetOrgId = "8evtad539nwKicxa1ExyWAn1nL4BK4Zu"; const targetUserId = "fM8PYi0lmiMrFvbNSL9kkCRbdlZ9tJBw"; const now = new Date(); @@ -2708,15 +2603,29 @@ export async function listStatusChangeRequestsForProject(projectId: string) { ); } -export async function listAllPendingStatusChangeRequests() { +export async function listAllPendingStatusChangeRequests(orgId: string | null) { + if (!orgId) { + return []; + } + const rows = await db .select({ requesterName: usersTable.name, req: statusChangeRequestsTable }) .from(statusChangeRequestsTable) + .innerJoin( + projectsTable, + eq(statusChangeRequestsTable.projectId, projectsTable.id) + ) + .innerJoin(clientsTable, eq(projectsTable.clientId, clientsTable.id)) .leftJoin( usersTable, eq(statusChangeRequestsTable.requestedBy, usersTable.id) ) - .where(eq(statusChangeRequestsTable.approvalState, "pending")) + .where( + and( + eq(statusChangeRequestsTable.approvalState, "pending"), + eq(clientsTable.organizationId, orgId) + ) + ) .orderBy(desc(statusChangeRequestsTable.createdAt)); return rows.map(({ req, requesterName }) => @@ -2929,81 +2838,18 @@ export async function listPortalActivityForUser(user: SessionUser) { }); } -// ── Portal Team / Colleague Invites ──────────────────────────────────────── - -export async function createPortalColleagueInvite(input: { - clientId: string; - email: string; - id: string; - initiatedByClientId: string; - token: string; -}) { - await db.insert(invitesTable).values({ - clientId: input.clientId, - createdAt: new Date(), - email: input.email, - expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), - id: input.id, - initiatedByClientId: input.initiatedByClientId, - token: input.token, - }); - - return getInviteRecordById(input.id); -} - -export async function listPortalTeam(user: SessionUser) { - // Get the client linked to this portal user - const [clientUser] = await db - .select({ client: clientsTable }) - .from(clientUsersTable) - .innerJoin(clientsTable, eq(clientUsersTable.clientId, clientsTable.id)) - .where(eq(clientUsersTable.userId, user.id)) - .limit(1); - - if (!clientUser) { - return { clientId: null, members: [], pendingInvites: [] }; - } - - const clientId = clientUser.client.id; - - const [members, pendingInvites] = await Promise.all([ - db - .select({ user: usersTable }) - .from(clientUsersTable) - .innerJoin(usersTable, eq(clientUsersTable.userId, usersTable.id)) - .where(eq(clientUsersTable.clientId, clientId)), - db - .select() - .from(invitesTable) - .where( - and( - eq(invitesTable.clientId, clientId), - isNull(invitesTable.consumedAt), - isNull(invitesTable.revokedAt), - gt(invitesTable.expiresAt, new Date()) - ) - ) - .orderBy(desc(invitesTable.createdAt)), - ]); - - return { - clientId, - members: members.map(({ user }) => ({ - createdAt: user.createdAt.toISOString(), - email: user.email, - id: user.id, - image: user.image, - name: user.name, - role: user.role, - })), - pendingInvites: pendingInvites.map((invite) => ({ - adminApprovedAt: invite.adminApprovedAt?.toISOString() ?? null, - clientId: invite.clientId, - createdAt: invite.createdAt.toISOString(), - email: invite.email, - expiresAt: invite.expiresAt.toISOString(), - id: invite.id, - initiatedByClientId: invite.initiatedByClientId, - })), - }; -} +// biome-ignore lint/performance/noBarrelFile: transitional re-export barrel during records.ts domain split (plan 006) +export { + approveInviteRecord, + consumeInvite, + createInviteRecord, + createPortalColleagueInvite, + getActiveInviteById, + getActiveInviteByToken, + getInviteRecordById, + linkUserToClient, + listPendingInvitesForClient, + listPortalTeam, + refreshInviteExpiration, + revokeInviteRecord, +} from "./invites"; diff --git a/src/db/schema.ts b/src/db/schema.ts index 1a19c46..baa821c 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -22,23 +22,31 @@ export const users = sqliteTable("users", { updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), }); -export const clients = sqliteTable("clients", { - id: text("id").primaryKey(), - name: text("name").notNull(), - company: text("company").notNull(), - email: text("email").notNull(), - phone: text("phone"), - website: text("website"), - status: text("status", { enum: ["active", "archived"] }) - .notNull() - .default("active"), - notes: text("notes"), - tags: text("tags"), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), - organizationId: text("organization_id").references(() => organizations.id, { - onDelete: "cascade", - }), -}); +export const clients = sqliteTable( + "clients", + { + id: text("id").primaryKey(), + name: text("name").notNull(), + company: text("company").notNull(), + email: text("email").notNull(), + phone: text("phone"), + website: text("website"), + status: text("status", { enum: ["active", "archived"] }) + .notNull() + .default("active"), + notes: text("notes"), + tags: text("tags"), + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), + organizationId: text("organization_id").references(() => organizations.id, { + onDelete: "cascade", + }), + }, + (table) => ({ + clientsOrganizationIdIdx: index("clients_organization_id_idx").on( + table.organizationId + ), + }) +); export const sessions = sqliteTable("session", { id: text("id").primaryKey(), @@ -104,27 +112,40 @@ export const clientUsers = sqliteTable( table.clientId, table.userId ), + clientUsersUserIdIdx: index("client_users_user_id_idx").on(table.userId), }) ); -export const invites = sqliteTable("invites", { - id: text("id").primaryKey(), - clientId: text("client_id") - .notNull() - .references(() => clients.id, { onDelete: "cascade" }), - email: text("email").notNull(), - token: text("token").notNull().unique(), - expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), - consumedAt: integer("consumed_at", { mode: "timestamp" }), - revokedAt: integer("revoked_at", { mode: "timestamp" }), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), - // Client-initiated colleague invite fields - initiatedByClientId: text("initiated_by_client_id").references( - () => clients.id, - { onDelete: "set null" } - ), - adminApprovedAt: integer("admin_approved_at", { mode: "timestamp" }), -}); +export const invites = sqliteTable( + "invites", + { + id: text("id").primaryKey(), + clientId: text("client_id") + .notNull() + .references(() => clients.id, { onDelete: "cascade" }), + email: text("email").notNull(), + token: text("token").notNull().unique(), + expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), + consumedAt: integer("consumed_at", { mode: "timestamp" }), + revokedAt: integer("revoked_at", { mode: "timestamp" }), + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), + // Client-initiated colleague invite fields + initiatedByClientId: text("initiated_by_client_id").references( + () => clients.id, + { onDelete: "set null" } + ), + adminApprovedAt: integer("admin_approved_at", { mode: "timestamp" }), + }, + (table) => ({ + invitesClientIdIdx: index("invites_client_id_idx").on(table.clientId), + // Speeds up pending-invite dedupe lookups, which filter by client + email + // before narrowing on consumed/revoked/expiry. + invitesClientEmailIdx: index("invites_client_email_idx").on( + table.clientId, + table.email + ), + }) +); export const projects = sqliteTable( "projects", @@ -151,94 +172,132 @@ export const projects = sqliteTable( }) ); -export const projectNotes = sqliteTable("project_notes", { - id: text("id").primaryKey(), - projectId: text("project_id") - .notNull() - .references(() => projects.id, { onDelete: "cascade" }), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - content: text("content").notNull(), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), -}); +export const projectNotes = sqliteTable( + "project_notes", + { + id: text("id").primaryKey(), + projectId: text("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + content: text("content").notNull(), + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), + }, + (table) => ({ + projectNotesProjectIdIdx: index("project_notes_project_id_idx").on( + table.projectId + ), + }) +); -export const projectUpdates = sqliteTable("project_updates", { - id: text("id").primaryKey(), - projectId: text("project_id") - .notNull() - .references(() => projects.id, { onDelete: "cascade" }), - authorId: text("author_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - title: text("title").notNull(), - body: text("body").notNull(), - status: text("status", { - enum: ["on_track", "at_risk", "blocked", "complete"], +export const projectUpdates = sqliteTable( + "project_updates", + { + id: text("id").primaryKey(), + projectId: text("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + authorId: text("author_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + title: text("title").notNull(), + body: text("body").notNull(), + status: text("status", { + enum: ["on_track", "at_risk", "blocked", "complete"], + }) + .notNull() + .default("on_track"), + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), + }, + (table) => ({ + projectUpdatesProjectIdIdx: index("project_updates_project_id_idx").on( + table.projectId + ), }) - .notNull() - .default("on_track"), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), - updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), -}); +); -export const projectMilestones = sqliteTable("project_milestones", { - id: text("id").primaryKey(), - projectId: text("project_id") - .notNull() - .references(() => projects.id, { onDelete: "cascade" }), - title: text("title").notNull(), - description: text("description"), - status: text("status", { - enum: ["todo", "in_progress", "done"], +export const projectMilestones = sqliteTable( + "project_milestones", + { + id: text("id").primaryKey(), + projectId: text("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + title: text("title").notNull(), + description: text("description"), + status: text("status", { + enum: ["todo", "in_progress", "done"], + }) + .notNull() + .default("todo"), + dueDate: text("due_date"), + sortOrder: integer("sort_order").notNull().default(0), + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), + }, + (table) => ({ + projectMilestonesProjectIdIdx: index( + "project_milestones_project_id_idx" + ).on(table.projectId), }) - .notNull() - .default("todo"), - dueDate: text("due_date"), - sortOrder: integer("sort_order").notNull().default(0), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), - updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), -}); +); -export const files = sqliteTable("files", { - id: text("id").primaryKey(), - projectId: text("project_id") - .notNull() - .references(() => projects.id, { onDelete: "cascade" }), - uploadedBy: text("uploaded_by") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - storageKey: text("storage_key").notNull().unique(), - fileUrl: text("file_url").notNull(), - fileName: text("file_name").notNull(), - fileSize: integer("file_size").notNull(), - mimeType: text("mime_type").notNull(), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), -}); +export const files = sqliteTable( + "files", + { + id: text("id").primaryKey(), + projectId: text("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + uploadedBy: text("uploaded_by") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + storageKey: text("storage_key").notNull().unique(), + fileUrl: text("file_url").notNull(), + fileName: text("file_name").notNull(), + fileSize: integer("file_size").notNull(), + mimeType: text("mime_type").notNull(), + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), + }, + (table) => ({ + filesProjectIdIdx: index("files_project_id_idx").on(table.projectId), + }) +); -export const statusChangeRequests = sqliteTable("status_change_requests", { - id: text("id").primaryKey(), - projectId: text("project_id") - .notNull() - .references(() => projects.id, { onDelete: "cascade" }), - requestedBy: text("requested_by") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - requestedStatus: text("requested_status", { - enum: ["planning", "in_progress", "completed"], - }).notNull(), - reason: text("reason").notNull(), - approvalState: text("approval_state", { - enum: ["pending", "approved", "rejected"], +export const statusChangeRequests = sqliteTable( + "status_change_requests", + { + id: text("id").primaryKey(), + projectId: text("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + requestedBy: text("requested_by") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + requestedStatus: text("requested_status", { + enum: ["planning", "in_progress", "completed"], + }).notNull(), + reason: text("reason").notNull(), + approvalState: text("approval_state", { + enum: ["pending", "approved", "rejected"], + }) + .notNull() + .default("pending"), + reviewedBy: text("reviewed_by").references(() => users.id, { + onDelete: "set null", + }), + reviewedAt: integer("reviewed_at", { mode: "timestamp" }), + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), + }, + (table) => ({ + statusChangeRequestsProjectIdIdx: index( + "status_change_requests_project_id_idx" + ).on(table.projectId), }) - .notNull() - .default("pending"), - reviewedBy: text("reviewed_by").references(() => users.id, { - onDelete: "set null", - }), - reviewedAt: integer("reviewed_at", { mode: "timestamp" }), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), -}); +); export const workspaceSettings = sqliteTable("workspace_settings", { id: text("id").primaryKey().default("default"), diff --git a/src/routes/api/admin/status-change-requests.ts b/src/routes/api/admin/status-change-requests.ts index 57b9531..0a44166 100644 --- a/src/routes/api/admin/status-change-requests.ts +++ b/src/routes/api/admin/status-change-requests.ts @@ -1,4 +1,5 @@ import { createFileRoute } from "@tanstack/react-router"; +import { ROLES } from "@/auth/roles"; import { listAllPendingStatusChangeRequests } from "@/db/records"; import { forbiddenError, @@ -13,10 +14,12 @@ export const Route = createFileRoute("/api/admin/status-change-requests")({ if (auth.error) { return auth.error; } - if (auth.user.role !== "admin") { + if (auth.user.role !== ROLES.ADMIN) { return forbiddenError("Admin only."); } - const requests = await listAllPendingStatusChangeRequests(); + const requests = await listAllPendingStatusChangeRequests( + auth.user.activeOrganizationId ?? null + ); return Response.json(requests); }, }, diff --git a/src/routes/api/admin/status-change-requests/$id.ts b/src/routes/api/admin/status-change-requests/$id.ts index 0f1a622..4717496 100644 --- a/src/routes/api/admin/status-change-requests/$id.ts +++ b/src/routes/api/admin/status-change-requests/$id.ts @@ -1,6 +1,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { reviewStatusChangeRequestSchema } from "@/api/validation"; import { + adminOwnsProject, getStatusChangeRequestById, reviewStatusChangeRequestRecord, } from "@/db/records"; @@ -29,7 +30,12 @@ export const Route = createFileRoute("/api/admin/status-change-requests/$id")({ } const requestRecord = await getStatusChangeRequestById(params.id); - if (!requestRecord) { + if ( + !( + requestRecord && + (await adminOwnsProject(auth.user, requestRecord.projectId)) + ) + ) { return notFoundError("Request not found"); } if (requestRecord.approvalState !== "pending") { diff --git a/src/routes/api/clients.ts b/src/routes/api/clients.ts index 8aaba3a..5792433 100644 --- a/src/routes/api/clients.ts +++ b/src/routes/api/clients.ts @@ -1,11 +1,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { createClientSchema } from "@/api/validation"; import { getSessionUserFromHeaders } from "@/auth/session.server"; -import { - createClientRecord, - listClientsForUser, - seedIfEmpty, -} from "@/db/records"; +import { createClientRecord, listClientsForUser } from "@/db/records"; import { forbiddenError, internalServerError, @@ -24,8 +20,6 @@ export const Route = createFileRoute("/api/clients")({ return unauthorizedError(); } - await seedIfEmpty(); - return Response.json(await listClientsForUser(user)); }, POST: async ({ request }) => { diff --git a/src/routes/api/clients/$id.ts b/src/routes/api/clients/$id.ts index 4b76b9e..3161ad1 100644 --- a/src/routes/api/clients/$id.ts +++ b/src/routes/api/clients/$id.ts @@ -2,6 +2,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { UTApi } from "uploadthing/server"; import { updateClientSchema } from "@/api/validation"; import { + adminOwnsClient, deleteClientRecord, listClientStorageKeys, updateClientRecord, @@ -52,6 +53,10 @@ export const Route = createFileRoute("/api/clients/$id")({ return parsed.error; } + if (!(await adminOwnsClient(auth.user, params.id))) { + return notFoundError("That client could not be found."); + } + const updated = await updateClientRecord(params.id, { ...parsed.data, id: params.id, @@ -70,6 +75,10 @@ export const Route = createFileRoute("/api/clients/$id")({ return auth.error; } + if (!(await adminOwnsClient(auth.user, params.id))) { + return notFoundError("That client could not be found."); + } + const storageKeys = await listClientStorageKeys(params.id); const deleted = await deleteClientRecord(params.id); diff --git a/src/routes/api/clients/$id/invites.ts b/src/routes/api/clients/$id/invites.ts index 79123eb..0b589be 100644 --- a/src/routes/api/clients/$id/invites.ts +++ b/src/routes/api/clients/$id/invites.ts @@ -1,8 +1,12 @@ import { createFileRoute } from "@tanstack/react-router"; import { ROLES } from "@/auth/roles"; import { getSessionUserFromHeaders } from "@/auth/session.server"; -import { listPendingInvitesForClient } from "@/db/records"; -import { forbiddenError, unauthorizedError } from "@/server/http/route-utils"; +import { adminOwnsClient, listPendingInvitesForClient } from "@/db/records"; +import { + forbiddenError, + notFoundError, + unauthorizedError, +} from "@/server/http/route-utils"; function serializePendingInvite( invite: Awaited>[number] @@ -34,6 +38,10 @@ export const Route = createFileRoute("/api/clients/$id/invites")({ return forbiddenError(); } + if (!(await adminOwnsClient(user, params.id))) { + return notFoundError("That client could not be found."); + } + const invites = await listPendingInvitesForClient(params.id); return Response.json(invites.map(serializePendingInvite)); diff --git a/src/routes/api/files/$id.ts b/src/routes/api/files/$id.ts index 08f62d7..89e8dc9 100644 --- a/src/routes/api/files/$id.ts +++ b/src/routes/api/files/$id.ts @@ -7,7 +7,6 @@ import { restoreProjectFileRecord, } from "@/db/records"; import { - forbiddenError, internalServerError, notFoundError, requireAdminMutationRequest, @@ -40,7 +39,7 @@ export const Route = createFileRoute("/api/files/$id")({ ); if (!hasProjectAccess) { - return forbiddenError("You do not have access to that file."); + return notFoundError("That file could not be found."); } const deletedRecord = await deleteProjectFileRecord(existing.id); diff --git a/src/routes/api/invites.ts b/src/routes/api/invites.ts index e74c028..95f02a0 100644 --- a/src/routes/api/invites.ts +++ b/src/routes/api/invites.ts @@ -1,6 +1,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { inviteSchema } from "@/api/validation"; import { + adminOwnsClient, createInviteRecord, getClientById, getInviteRecordById, @@ -30,6 +31,10 @@ export const Route = createFileRoute("/api/invites")({ return parsed.error; } + if (!(await adminOwnsClient(auth.user, parsed.data.clientId))) { + return notFoundError("We could not find a client with that id."); + } + const client = await getClientById(parsed.data.clientId); if (!client) { diff --git a/src/routes/api/invites/$id/approve.ts b/src/routes/api/invites/$id/approve.ts index 95d4a0e..8e1cfdd 100644 --- a/src/routes/api/invites/$id/approve.ts +++ b/src/routes/api/invites/$id/approve.ts @@ -1,7 +1,13 @@ import { createFileRoute } from "@tanstack/react-router"; -import { approveInviteRecord, getClientById } from "@/db/records"; +import { + adminOwnsClient, + approveInviteRecord, + getClientById, + getInviteRecordById, +} from "@/db/records"; import { sendInviteEmail } from "@/server/email/notifications"; import { + conflictError, notFoundError, requireAdminMutationRequest, } from "@/server/http/route-utils"; @@ -17,6 +23,23 @@ export const Route = createFileRoute("/api/invites/$id/approve")({ return auth.error; } + const pendingInvite = await getInviteRecordById(id); + + if ( + !( + pendingInvite && + (await adminOwnsClient(auth.user, pendingInvite.clientId)) + ) + ) { + return notFoundError( + "That pending invite could not be found or approved." + ); + } + + if (pendingInvite.adminApprovedAt) { + return conflictError("That invite has already been approved."); + } + const approvedInvite = await approveInviteRecord(id); if (!approvedInvite) { diff --git a/src/routes/api/invites/$id/resend.ts b/src/routes/api/invites/$id/resend.ts index af10ba2..d803089 100644 --- a/src/routes/api/invites/$id/resend.ts +++ b/src/routes/api/invites/$id/resend.ts @@ -1,11 +1,13 @@ import { createFileRoute } from "@tanstack/react-router"; import { + adminOwnsClient, getActiveInviteById, getClientById, refreshInviteExpiration, } from "@/db/records"; import { sendInviteEmail } from "@/server/email/notifications"; import { + conflictError, internalServerError, notFoundError, requireAdminMutationRequest, @@ -24,7 +26,7 @@ export const Route = createFileRoute("/api/invites/$id/resend")({ const invite = await getActiveInviteById(id); - if (!invite) { + if (!(invite && (await adminOwnsClient(auth.user, invite.clientId)))) { return notFoundError("That pending invite could not be found."); } @@ -34,14 +36,32 @@ export const Route = createFileRoute("/api/invites/$id/resend")({ return notFoundError("The invited client could not be found."); } - const inviteUrl = new URL(`/invite/${invite.token}`, request.url); + if (invite.initiatedByClientId && !invite.adminApprovedAt) { + return conflictError( + "Approve this colleague invite before resending the invite email." + ); + } + + const refreshedInvite = await refreshInviteExpiration( + invite.id, + new Date(Date.now() + 1000 * 60 * 60 * 24 * 7) + ); + + if (!refreshedInvite) { + return internalServerError("Invite could not be refreshed."); + } + + const inviteUrl = new URL( + `/invite/${refreshedInvite.token}`, + request.url + ); try { await sendInviteEmail({ clientCompany: client.company, clientName: client.name, - email: invite.email, - inviteId: invite.id, + email: refreshedInvite.email, + inviteId: refreshedInvite.id, inviteUrl: inviteUrl.toString(), requestUrl: request.url, }); @@ -50,20 +70,11 @@ export const Route = createFileRoute("/api/invites/$id/resend")({ return internalServerError("Invite email could not be sent."); } - const refreshedInvite = await refreshInviteExpiration( - invite.id, - new Date(Date.now() + 1000 * 60 * 60 * 24 * 7) - ); - - if (!refreshedInvite) { - return internalServerError("Invite could not be refreshed."); - } - return Response.json({ clientId: refreshedInvite.clientId, - createdAt: refreshedInvite.createdAt, + createdAt: refreshedInvite.createdAt.toISOString(), email: refreshedInvite.email, - expiresAt: refreshedInvite.expiresAt, + expiresAt: refreshedInvite.expiresAt.toISOString(), id: refreshedInvite.id, }); }, diff --git a/src/routes/api/invites/$id/revoke.ts b/src/routes/api/invites/$id/revoke.ts index 958ec96..de13e72 100644 --- a/src/routes/api/invites/$id/revoke.ts +++ b/src/routes/api/invites/$id/revoke.ts @@ -1,5 +1,9 @@ import { createFileRoute } from "@tanstack/react-router"; -import { revokeInviteRecord } from "@/db/records"; +import { + adminOwnsClient, + getInviteRecordById, + revokeInviteRecord, +} from "@/db/records"; import { notFoundError, requireAdminMutationRequest, @@ -16,6 +20,17 @@ export const Route = createFileRoute("/api/invites/$id/revoke")({ return auth.error; } + const pendingInvite = await getInviteRecordById(id); + + if ( + !( + pendingInvite && + (await adminOwnsClient(auth.user, pendingInvite.clientId)) + ) + ) { + return notFoundError("That pending invite could not be found."); + } + const invite = await revokeInviteRecord(id); if (!invite) { diff --git a/src/routes/api/portal/summary.ts b/src/routes/api/portal/summary.ts index d3de8db..9dee91a 100644 --- a/src/routes/api/portal/summary.ts +++ b/src/routes/api/portal/summary.ts @@ -1,7 +1,8 @@ import { createFileRoute } from "@tanstack/react-router"; +import { ROLES } from "@/auth/roles"; import { getSessionUserFromHeaders } from "@/auth/session.server"; -import { getPortalSummary, seedIfEmpty } from "@/db/records"; -import { unauthorizedError } from "@/server/http/route-utils"; +import { getPortalSummary } from "@/db/records"; +import { forbiddenError, unauthorizedError } from "@/server/http/route-utils"; export const Route = createFileRoute("/api/portal/summary")({ server: { @@ -13,7 +14,9 @@ export const Route = createFileRoute("/api/portal/summary")({ return unauthorizedError(); } - await seedIfEmpty(); + if (user.role !== ROLES.CLIENT) { + return forbiddenError("Client portal only."); + } return Response.json(await getPortalSummary(user)); }, diff --git a/src/routes/api/portal/team.ts b/src/routes/api/portal/team.ts index 4ccd024..feb831b 100644 --- a/src/routes/api/portal/team.ts +++ b/src/routes/api/portal/team.ts @@ -1,13 +1,8 @@ import { createFileRoute } from "@tanstack/react-router"; import { portalInviteSchema } from "@/api/validation"; +import { ROLES } from "@/auth/roles"; import { getSessionUserFromHeaders } from "@/auth/session.server"; -import { - createPortalColleagueInvite, - getClientById, - listPortalTeam, -} from "@/db/records"; - -import { sendInviteEmail } from "@/server/email/notifications"; +import { createPortalColleagueInvite, listPortalTeam } from "@/db/records"; import { forbiddenError, internalServerError, @@ -16,6 +11,20 @@ import { unauthorizedError, } from "@/server/http/route-utils"; +function serializePortalColleagueInvite( + invite: NonNullable>> +) { + return { + adminApprovedAt: invite.adminApprovedAt?.toISOString() ?? null, + clientId: invite.clientId, + createdAt: invite.createdAt.toISOString(), + email: invite.email, + expiresAt: invite.expiresAt.toISOString(), + id: invite.id, + initiatedByClientId: invite.initiatedByClientId, + }; +} + export const Route = createFileRoute("/api/portal/team")({ server: { handlers: { @@ -24,7 +33,7 @@ export const Route = createFileRoute("/api/portal/team")({ if (!user) { return unauthorizedError(); } - if (user.role !== "client") { + if (user.role !== ROLES.CLIENT) { return forbiddenError("Client portal only."); } const team = await listPortalTeam(user); @@ -36,7 +45,7 @@ export const Route = createFileRoute("/api/portal/team")({ if (auth.error) { return auth.error; } - if (auth.user.role !== "client") { + if (auth.user.role !== ROLES.CLIENT) { return forbiddenError("Client portal only."); } @@ -51,11 +60,6 @@ export const Route = createFileRoute("/api/portal/team")({ return forbiddenError("No client linked to your account."); } - const client = await getClientById(team.clientId); - if (!client) { - return internalServerError("Client not found."); - } - const token = crypto.randomUUID(); const inviteId = crypto.randomUUID(); @@ -71,23 +75,11 @@ export const Route = createFileRoute("/api/portal/team")({ return internalServerError("Invite could not be created."); } - const inviteUrl = new URL(`/invite/${token}`, request.url); - - try { - await sendInviteEmail({ - clientCompany: client.company, - clientName: client.name, - email: invite.email, - inviteId: invite.id, - inviteUrl: inviteUrl.toString(), - requestUrl: request.url, - }); - } catch (error) { - console.error("portal invite email failed", error); - // Don't roll back — the invite is still valid, email is best-effort - } + const isNew = invite.id === inviteId; - return Response.json(invite, { status: 201 }); + return Response.json(serializePortalColleagueInvite(invite), { + status: isNew ? 201 : 200, + }); }, }, }, diff --git a/src/routes/api/project-milestones/$id.ts b/src/routes/api/project-milestones/$id.ts index cd24713..9a7cac7 100644 --- a/src/routes/api/project-milestones/$id.ts +++ b/src/routes/api/project-milestones/$id.ts @@ -1,6 +1,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { projectMilestoneSchema } from "@/api/validation"; import { + adminOwnsProjectMilestone, deleteProjectMilestoneRecord, serializeProjectMilestone, updateProjectMilestoneRecord, @@ -30,6 +31,10 @@ export const Route = createFileRoute("/api/project-milestones/$id")({ return parsed.error; } + if (!(await adminOwnsProjectMilestone(auth.user, params.id))) { + return notFoundError("That milestone could not be found."); + } + const updated = await updateProjectMilestoneRecord( params.id, parsed.data @@ -51,6 +56,10 @@ export const Route = createFileRoute("/api/project-milestones/$id")({ return auth.error; } + if (!(await adminOwnsProjectMilestone(auth.user, params.id))) { + return notFoundError("That milestone could not be found."); + } + const deleted = await deleteProjectMilestoneRecord(params.id); if (!deleted) { diff --git a/src/routes/api/project-updates/$id.ts b/src/routes/api/project-updates/$id.ts index 099b916..c59ec2a 100644 --- a/src/routes/api/project-updates/$id.ts +++ b/src/routes/api/project-updates/$id.ts @@ -1,6 +1,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { projectUpdateSchema } from "@/api/validation"; import { + adminOwnsProjectUpdate, deleteProjectUpdateRecord, serializeProjectUpdate, updateProjectUpdateRecord, @@ -30,6 +31,10 @@ export const Route = createFileRoute("/api/project-updates/$id")({ return parsed.error; } + if (!(await adminOwnsProjectUpdate(auth.user, params.id))) { + return notFoundError("That project update could not be found."); + } + const updated = await updateProjectUpdateRecord(params.id, parsed.data); if (!updated) { @@ -48,6 +53,10 @@ export const Route = createFileRoute("/api/project-updates/$id")({ return auth.error; } + if (!(await adminOwnsProjectUpdate(auth.user, params.id))) { + return notFoundError("That project update could not be found."); + } + const deleted = await deleteProjectUpdateRecord(params.id); if (!deleted) { diff --git a/src/routes/api/projects.ts b/src/routes/api/projects.ts index f4c2ea6..3a02385 100644 --- a/src/routes/api/projects.ts +++ b/src/routes/api/projects.ts @@ -2,14 +2,15 @@ import { createFileRoute } from "@tanstack/react-router"; import { createProjectSchema } from "@/api/validation"; import { getSessionUserFromHeaders } from "@/auth/session.server"; import { + adminOwnsClient, createProjectRecord, DuplicateProjectSlugError, listProjectsForUser, - seedIfEmpty, } from "@/db/records"; import { conflictError, internalServerError, + notFoundError, parseJsonBody, requireAdminMutationRequest, unauthorizedError, @@ -25,8 +26,6 @@ export const Route = createFileRoute("/api/projects")({ return unauthorizedError(); } - await seedIfEmpty(); - return Response.json(await listProjectsForUser(user)); }, POST: async ({ request }) => { @@ -42,6 +41,10 @@ export const Route = createFileRoute("/api/projects")({ return parsed.error; } + if (!(await adminOwnsClient(auth.user, parsed.data.clientId))) { + return notFoundError("We could not find a client with that id."); + } + let created: Awaited>; try { diff --git a/src/routes/api/projects/$id.ts b/src/routes/api/projects/$id.ts index f82b0aa..ecaf031 100644 --- a/src/routes/api/projects/$id.ts +++ b/src/routes/api/projects/$id.ts @@ -2,6 +2,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { UTApi } from "uploadthing/server"; import { updateProjectSchema } from "@/api/validation"; import { + adminOwnsProject, DuplicateProjectSlugError, deleteProjectRecord, listProjectStorageKeys, @@ -54,6 +55,10 @@ export const Route = createFileRoute("/api/projects/$id")({ return parsed.error; } + if (!(await adminOwnsProject(auth.user, params.id))) { + return notFoundError("That project could not be found."); + } + let updated: Awaited>; try { @@ -82,6 +87,10 @@ export const Route = createFileRoute("/api/projects/$id")({ return auth.error; } + if (!(await adminOwnsProject(auth.user, params.id))) { + return notFoundError("That project could not be found."); + } + const storageKeys = await listProjectStorageKeys(params.id); const deleted = await deleteProjectRecord(params.id); diff --git a/src/routes/api/projects/$id/collaboration.ts b/src/routes/api/projects/$id/collaboration.ts index c4eebc6..a77f677 100644 --- a/src/routes/api/projects/$id/collaboration.ts +++ b/src/routes/api/projects/$id/collaboration.ts @@ -1,10 +1,6 @@ import { createFileRoute } from "@tanstack/react-router"; import { getSessionUserFromHeaders } from "@/auth/session.server"; -import { - canAccessProject, - getProjectCollaboration, - seedIfEmpty, -} from "@/db/records"; +import { canAccessProject, getProjectCollaboration } from "@/db/records"; import { forbiddenError, notFoundError, @@ -21,8 +17,6 @@ export const Route = createFileRoute("/api/projects/$id/collaboration")({ return unauthorizedError(); } - await seedIfEmpty(); - const hasAccess = await canAccessProject(user, params.id); if (!hasAccess) { diff --git a/src/routes/api/projects/$id/milestones.ts b/src/routes/api/projects/$id/milestones.ts index ac34d7f..ced2afb 100644 --- a/src/routes/api/projects/$id/milestones.ts +++ b/src/routes/api/projects/$id/milestones.ts @@ -2,8 +2,8 @@ import { createFileRoute } from "@tanstack/react-router"; import { projectMilestoneSchema } from "@/api/validation"; import { getSessionUserFromHeaders } from "@/auth/session.server"; import { + adminOwnsProject, createProjectMilestoneRecord, - getProjectById, listProjectMilestonesForUser, serializeProjectMilestone, } from "@/db/records"; @@ -44,9 +44,7 @@ export const Route = createFileRoute("/api/projects/$id/milestones")({ return auth.error; } - const project = await getProjectById(params.id); - - if (!project) { + if (!(await adminOwnsProject(auth.user, params.id))) { return notFoundError("We could not find a project with that id."); } diff --git a/src/routes/api/projects/$id/updates.ts b/src/routes/api/projects/$id/updates.ts index 37bf372..5bec70a 100644 --- a/src/routes/api/projects/$id/updates.ts +++ b/src/routes/api/projects/$id/updates.ts @@ -2,8 +2,8 @@ import { createFileRoute } from "@tanstack/react-router"; import { projectUpdateSchema } from "@/api/validation"; import { getSessionUserFromHeaders } from "@/auth/session.server"; import { + adminOwnsProject, createProjectUpdateRecord, - getProjectById, getProjectNotificationContext, listProjectUpdatesForUser, serializeProjectUpdate, @@ -49,9 +49,7 @@ export const Route = createFileRoute("/api/projects/$id/updates")({ return auth.error; } - const project = await getProjectById(params.id); - - if (!project) { + if (!(await adminOwnsProject(auth.user, params.id))) { return notFoundError("We could not find a project with that id."); } diff --git a/src/routes/api/search.ts b/src/routes/api/search.ts index 20d6639..5797f1b 100644 --- a/src/routes/api/search.ts +++ b/src/routes/api/search.ts @@ -2,7 +2,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { ZodError } from "zod"; import { searchSchema } from "@/api/validation"; import { getSessionUserFromHeaders } from "@/auth/session.server"; -import { searchRecords, seedIfEmpty } from "@/db/records"; +import { searchRecords } from "@/db/records"; import { unauthorizedError, validationError } from "@/server/http/route-utils"; export const Route = createFileRoute("/api/search")({ @@ -34,7 +34,6 @@ export const Route = createFileRoute("/api/search")({ return Response.json({ clients: [], projects: [] }); } - await seedIfEmpty(); return Response.json(await searchRecords(query, user)); }, }, diff --git a/src/routes/api/users/$id.ts b/src/routes/api/users/$id.ts index 94ac7f2..c452f01 100644 --- a/src/routes/api/users/$id.ts +++ b/src/routes/api/users/$id.ts @@ -1,6 +1,6 @@ import { createFileRoute } from "@tanstack/react-router"; import { updateUserRoleSchema } from "@/api/validation"; -import { deleteUserById, setUserRole } from "@/db/records"; +import { adminManagesUser, deleteUserById, setUserRole } from "@/db/records"; import { forbiddenError, notFoundError, @@ -28,6 +28,10 @@ export const Route = createFileRoute("/api/users/$id")({ return parsed.error; } + if (!(await adminManagesUser(auth.user, params.id))) { + return notFoundError("That user could not be found."); + } + const updated = await setUserRole(params.id, parsed.data.role); if (!updated) { @@ -47,6 +51,10 @@ export const Route = createFileRoute("/api/users/$id")({ return forbiddenError("You cannot delete your own account."); } + if (!(await adminManagesUser(auth.user, params.id))) { + return notFoundError("That user could not be found."); + } + const deleted = await deleteUserById(params.id); if (!deleted) { diff --git a/src/routes/portal/activity.tsx b/src/routes/portal/activity.tsx index 553a5c9..6ad2507 100644 --- a/src/routes/portal/activity.tsx +++ b/src/routes/portal/activity.tsx @@ -76,6 +76,13 @@ function ActivityCard({ item }: { item: PortalActivityItem }) { const update = isUpdate ? (item.data as ProjectUpdate) : null; const file = isFile ? (item.data as ProjectFile) : null; + let activityIcon = NotificationSquareIcon; + if (isComment) { + activityIcon = Comment01Icon; + } else if (isFile) { + activityIcon = FileUploadIcon; + } + return (
{/* Icon */} @@ -89,16 +96,7 @@ function ActivityCard({ item }: { item: PortalActivityItem }) { "border-teal-200 bg-teal-50 text-teal-600 dark:border-teal-900 dark:bg-teal-950/20" )} > - +
{/* Content */} @@ -250,10 +248,10 @@ function PortalActivityPage() { {/* Activity list */}
- {items.map((item, i) => ( + {items.map((item) => ( ))}
diff --git a/src/routes/portal/files.tsx b/src/routes/portal/files.tsx index 312a642..b8ff210 100644 --- a/src/routes/portal/files.tsx +++ b/src/routes/portal/files.tsx @@ -173,7 +173,10 @@ function PortalFilesPage() { if (!byProject.has(file.projectId)) { byProject.set(file.projectId, { title: file.projectTitle, files: [] }); } - byProject.get(file.projectId)!.files.push(file); + const projectGroup = byProject.get(file.projectId); + if (projectGroup) { + projectGroup.files.push(file); + } } const projects = projectsQuery.data ?? []; diff --git a/src/routes/portal/projects/$id.tsx b/src/routes/portal/projects/$id.tsx index b8612f2..e09e71d 100644 --- a/src/routes/portal/projects/$id.tsx +++ b/src/routes/portal/projects/$id.tsx @@ -10,8 +10,9 @@ import { } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { createFileRoute } from "@tanstack/react-router"; -import { type FormEvent, useState } from "react"; +import { type FormEvent, type ReactNode, useState } from "react"; import { requireClientSession } from "@/auth/guards"; +import { ROLES } from "@/auth/roles"; import { PortalProjectDetailPendingPage } from "@/components/common/route-pending"; import { EmptyPanel, @@ -161,7 +162,78 @@ function PremiumDeadlineCard({ deadline }: { deadline: string }) { function PrimarySuccessTeamWidget() { const usersQuery = useUsersData(); const users = usersQuery.data ?? []; - const admins = users.filter((u) => u.role === "admin"); + const admins = users.filter((u) => u.role === ROLES.ADMIN); + + let successTeamBody: ReactNode; + if (usersQuery.isLoading) { + successTeamBody = ( +
+
+
+
+
+
+
+
+
+ ); + } else if (admins.length === 0) { + successTeamBody = ( +

+ No success team members assigned. +

+ ); + } else { + successTeamBody = ( +
+ {admins.slice(0, 2).map((admin) => ( +
+
+ {admin.image ? ( + {admin.name} + ) : ( +
+ {admin.name + .split(" ") + .map((n) => n[0]) + .slice(0, 2) + .join("") + .toUpperCase()} +
+ )} +
+

+ {admin.name} +

+ + Agency Expert + +
+
+ +
+ + + +
+
+ ))} +
+ ); + } return (
@@ -174,69 +246,7 @@ function PrimarySuccessTeamWidget() { icon={UserGroupIcon} />
- {usersQuery.isLoading ? ( -
-
-
-
-
-
-
-
-
- ) : admins.length === 0 ? ( -

- No success team members assigned. -

- ) : ( -
- {admins.slice(0, 2).map((admin) => ( -
-
- {admin.image ? ( - {admin.name} - ) : ( -
- {admin.name - .split(" ") - .map((n) => n[0]) - .slice(0, 2) - .join("") - .toUpperCase()} -
- )} -
-

- {admin.name} -

- - Agency Expert - -
-
- -
- - - -
-
- ))} -
- )} + {successTeamBody}
); } diff --git a/src/routes/projects/$id.tsx b/src/routes/projects/$id.tsx index e2cca6e..d09b0c9 100644 --- a/src/routes/projects/$id.tsx +++ b/src/routes/projects/$id.tsx @@ -384,6 +384,7 @@ function MilestoneAnalyticsWidget({ ); } +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: tracked for extraction in plan 006 export function AdminProjectDetailPage({ clientSlug, projectSlug,