From 01d1e5fb604f403b4a02483ea076c007c1b54918 Mon Sep 17 00:00:00 2001
From: Pascal Syma
Date: Thu, 13 Aug 2026 23:07:57 +0200
Subject: [PATCH] test(integration): add real-stack integration test suite with
PGlite
- PGlite (in-memory WASM Postgres) hosted in a child process, exposed
via TCP socket; real migrations applied with prisma migrate deploy
- Vitest harness (vitest.integration.config.ts, global-setup spawns
PGlite + built Nuxt server, per-test TRUNCATE, factories, cookie-jar
API client); no mocks, zero production code changes
- 50 tests across auth, hunt, team, submit, review, showcase, diashow
incl. SuperJSON envelope check; *.itest.ts keeps bun test untouched
- CI: parallel unit + integration jobs in .github/workflows/test.yml
- tests/ covered by nuxt typecheck via tests/tsconfig.json reference
---
.github/workflows/test.yml | 40 ++++++
.gitignore | 1 +
bun.lock | 215 +++++++++++++++++++++-------
package.json | 4 +
tests/helpers/api.ts | 69 +++++++++
tests/helpers/database.ts | 19 +++
tests/helpers/factories.ts | 147 +++++++++++++++++++
tests/integration/auth.itest.ts | 124 ++++++++++++++++
tests/integration/diashow.itest.ts | 137 ++++++++++++++++++
tests/integration/global-setup.ts | 140 ++++++++++++++++++
tests/integration/health.itest.ts | 11 ++
tests/integration/hunt.itest.ts | 183 +++++++++++++++++++++++
tests/integration/pglite-host.mjs | 26 ++++
tests/integration/review.itest.ts | 186 ++++++++++++++++++++++++
tests/integration/setup.ts | 11 ++
tests/integration/showcase.itest.ts | 145 +++++++++++++++++++
tests/integration/submit.itest.ts | 195 +++++++++++++++++++++++++
tests/integration/team.itest.ts | 141 ++++++++++++++++++
tests/integration/types.d.ts | 9 ++
tests/tsconfig.json | 33 +++++
tsconfig.json | 3 +
vitest.integration.config.ts | 23 +++
22 files changed, 1813 insertions(+), 49 deletions(-)
create mode 100644 .github/workflows/test.yml
create mode 100644 tests/helpers/api.ts
create mode 100644 tests/helpers/database.ts
create mode 100644 tests/helpers/factories.ts
create mode 100644 tests/integration/auth.itest.ts
create mode 100644 tests/integration/diashow.itest.ts
create mode 100644 tests/integration/global-setup.ts
create mode 100644 tests/integration/health.itest.ts
create mode 100644 tests/integration/hunt.itest.ts
create mode 100644 tests/integration/pglite-host.mjs
create mode 100644 tests/integration/review.itest.ts
create mode 100644 tests/integration/setup.ts
create mode 100644 tests/integration/showcase.itest.ts
create mode 100644 tests/integration/submit.itest.ts
create mode 100644 tests/integration/team.itest.ts
create mode 100644 tests/integration/types.d.ts
create mode 100644 tests/tsconfig.json
create mode 100644 vitest.integration.config.ts
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..b48082d
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,40 @@
+name: Tests
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ unit:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Install bun
+ uses: oven-sh/setup-bun@v2
+ - name: Install dependencies
+ run: bun install --include=dev
+ - name: Check formatting
+ run: bun run check
+ - name: ESLint
+ run: bun run lint
+ - name: Typecheck
+ run: bun run typecheck
+ - name: Run tests
+ run: bun run test
+ integration:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Install bun
+ uses: oven-sh/setup-bun@v2
+ - name: Setup Node
+ # The built Nitro server and the WASM Postgres host run under node.
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ - name: Install dependencies
+ run: bun install --include=dev
+ - name: Run integration tests
+ run: bun run test:integration
diff --git a/.gitignore b/.gitignore
index 964f052..33c18f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,4 @@ logs
!.env.example
shared/generated/prisma
+*.tsbuildinfo
diff --git a/bun.lock b/bun.lock
index 944941b..bd4c248 100644
--- a/bun.lock
+++ b/bun.lock
@@ -33,12 +33,15 @@
"zod": "^4.4.3",
},
"devDependencies": {
+ "@electric-sql/pglite": "^0.5.5",
+ "@electric-sql/pglite-socket": "^0.2.8",
"@nuxt/devtools": "^2.6.4",
"@types/bun": "^1.2.20",
"prettier": "^3.6.2",
"prettier-plugin-organize-imports": "^4.2.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"prisma": "^6.16.1",
+ "vitest": "^4.1.10",
"vue-tsc": "^3.0.6",
},
},
@@ -131,6 +134,24 @@
"@dependents/detective-less": ["@dependents/detective-less@5.0.1", "", { "dependencies": { "gonzales-pe": "^4.3.0", "node-source-walk": "^7.0.1" } }, "sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ=="],
+ "@electric-sql/pglite": ["@electric-sql/pglite@0.5.5", "", {}, "sha512-QeZ+oB7QaU4EJj2awrB8hwFZ5TXxLRBQ9Gfq0dQauYDdWsRtuWRCERgtri35vRfL07KQHlVlc/4Qxvn7a5AXeA=="],
+
+ "@electric-sql/pglite-age": ["@electric-sql/pglite-age@0.0.6", "", { "peerDependencies": { "@electric-sql/pglite": "0.5.5" } }, "sha512-cnj+KxJUgx6WdwFgIbu2wQSu5NKNOf1TNfLAwVTHZZGkTTyA8FW8FrFZ06jKUkKoW7Fad8X0O4FPb/I+u5n5Rw=="],
+
+ "@electric-sql/pglite-pg_hashids": ["@electric-sql/pglite-pg_hashids@0.0.6", "", { "peerDependencies": { "@electric-sql/pglite": "0.5.5" } }, "sha512-723NvdqXz70rTjC5OKQHmmNNogTcZxPLAJf0NRDanz3zz77nBOCSlTL/UKkah90UVk5kqnbrwIcHqDVGmmONDA=="],
+
+ "@electric-sql/pglite-pg_ivm": ["@electric-sql/pglite-pg_ivm@0.0.6", "", { "peerDependencies": { "@electric-sql/pglite": "0.5.5" } }, "sha512-LroA6kGyIEeB1/P7RbAaDHDCgu6+0scXzNe4ahN73f/PfexVN4h1k7HXOUS2su7tAHVMOBRlfenaWl3J/KhSmg=="],
+
+ "@electric-sql/pglite-pg_textsearch": ["@electric-sql/pglite-pg_textsearch@0.0.7", "", { "peerDependencies": { "@electric-sql/pglite": "0.5.5" } }, "sha512-gBWaCxBcFPrmecgnycTXVJrAM39qEsXqZEuQZg1itlnIti1233htFYkOg9xp+FFE8R0Z7XmjbSJxjFoG60LD7w=="],
+
+ "@electric-sql/pglite-pg_uuidv7": ["@electric-sql/pglite-pg_uuidv7@0.0.6", "", { "peerDependencies": { "@electric-sql/pglite": "0.5.5" } }, "sha512-ffQARR67C9gFIhoaqSF/J0IUM6STJASrmcRvhlOokv414WBkl+wiNHBslw7hc3tYSZmg41Y7L1tjxNPNZn1wPA=="],
+
+ "@electric-sql/pglite-pgtap": ["@electric-sql/pglite-pgtap@0.0.6", "", { "peerDependencies": { "@electric-sql/pglite": "0.5.5" } }, "sha512-2Xn9xevILlNHTAeLZJnrB7DtOoB2qEiXm+tPaP0cm4n1a9zsAb4HQBRgsQhWKF7PJn9qMGJYFMuWWoMaxLTXwA=="],
+
+ "@electric-sql/pglite-pgvector": ["@electric-sql/pglite-pgvector@0.0.6", "", { "peerDependencies": { "@electric-sql/pglite": "0.5.5" } }, "sha512-KiPeiS/BMmjO71igzgexJAu5aPb3Y2yUjNTSMdti4ZL3zlqZFl4eLgbzLBGyG5Cxr9xFwYFmHh61ZTq6w6hUJA=="],
+
+ "@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.2.8", "", { "peerDependencies": { "@electric-sql/pglite": "0.5.5", "@electric-sql/pglite-age": "0.0.6", "@electric-sql/pglite-pg_hashids": "0.0.6", "@electric-sql/pglite-pg_ivm": "0.0.6", "@electric-sql/pglite-pg_textsearch": "0.0.7", "@electric-sql/pglite-pg_uuidv7": "0.0.6", "@electric-sql/pglite-pgtap": "0.0.6", "@electric-sql/pglite-pgvector": "0.0.6" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-s/hDC799oT1YLrQoyfiBKqwFcIn0m6OUEuU2ENdzr3uxW5rJBM73hpQeiBmSs+pqDD2A3PH4a8839ExqoROVmQ=="],
+
"@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
"@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
@@ -263,7 +284,7 @@
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.10", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q=="],
- "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@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=="],
@@ -607,7 +628,7 @@
"@speed-highlight/core": ["@speed-highlight/core@1.2.17", "", {}, "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg=="],
- "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.2.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/types": "^8.38.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-oY7GVkJGVMI5benlBDCaRrSC1qPasafyv5dOBLLv5MTilMGnErKhO6ziEfodDDIZbo5QxPUNW360VudJOFODMw=="],
@@ -617,6 +638,10 @@
"@types/bytes": ["@types/bytes@3.1.5", "", {}, "sha512-VgZkrJckypj85YxEsEavcMmmSOIzkUHqWmM4CCyia5dc54YwsXzJ5uT4fYxBQNEXx+oF1krlhgCbvfubXqZYsQ=="],
+ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
+
+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
@@ -707,6 +732,20 @@
"@vitejs/plugin-vue-jsx": ["@vitejs/plugin-vue-jsx@5.1.6", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-syntax-typescript": "^7.29.7", "@babel/plugin-transform-typescript": "^7.29.7", "@rolldown/pluginutils": "^1.0.1", "@vue/babel-plugin-jsx": "^2.0.1" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.0.0" } }, "sha512-YXvi4as2clxt6DFw5+a0tTA97ntiQXm/raR8ofNj3aNwwdlVGTiG2gp7EvfZW17P50acL/9bP0ccF4XnqNmlgA=="],
+ "@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="],
+
+ "@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "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-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="],
+
+ "@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="],
+
+ "@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="],
+
+ "@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="],
+
+ "@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="],
+
+ "@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="],
+
"@volar/language-core": ["@volar/language-core@2.4.23", "", { "dependencies": { "@volar/source-map": "2.4.23" } }, "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ=="],
"@volar/source-map": ["@volar/source-map@2.4.23", "", {}, "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q=="],
@@ -815,6 +854,8 @@
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
"ast-kit": ["ast-kit@1.4.3", "", { "dependencies": { "@babel/parser": "^7.27.0", "pathe": "^2.0.3" } }, "sha512-MdJqjpodkS5J149zN0Po+HPshkTdUyrvF7CKTafUgv69vBSPtncrj+3IiUgqdd7ElIEkbeXCsEouBUwLrw9Ilg=="],
"ast-module-types": ["ast-module-types@6.0.1", "", {}, "sha512-WHw67kLXYbZuHTmcdbIrVArCq5wxo6NEuj3hiYAWr8mwJeC+C2mMCIBIWCiDoCye/OF/xelc+teJ1ERoWmnEIA=="],
@@ -903,6 +944,8 @@
"case-anything": ["case-anything@3.1.2", "", {}, "sha512-wljhAjDDIv/hM2FzgJnYQg90AWmZMNtESCjTeLH680qTzdo0nErlCxOmgzgX4ZsZAtIvqHyD87ES8QyriXB+BQ=="],
+ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
+
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="],
@@ -1197,6 +1240,8 @@
"execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
+ "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="],
+
"exsolve": ["exsolve@1.1.0", "", {}, "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw=="],
"externality": ["externality@1.0.2", "", { "dependencies": { "enhanced-resolve": "^5.14.1", "mlly": "^1.3.0", "pathe": "^1.1.1", "ufo": "^1.1.2" } }, "sha512-LyExtJWKxtgVzmgtEHyQtLFpw1KFhQphF9nTG8TpAIVkiI/xQ3FJh75tRFLYl4hkn7BNIIdLJInuDAavX35pMw=="],
@@ -1567,7 +1612,7 @@
"magic-regexp": ["magic-regexp@0.10.0", "", { "dependencies": { "estree-walker": "^3.0.3", "magic-string": "^0.30.12", "mlly": "^1.7.2", "regexp-tree": "^0.1.27", "type-level-regexp": "~0.1.17", "ufo": "^1.5.4", "unplugin": "^2.0.0" } }, "sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg=="],
- "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"magic-string-ast": ["magic-string-ast@0.7.1", "", { "dependencies": { "magic-string": "^0.30.17" } }, "sha512-ub9iytsEbT7Yw/Pd29mSo/cNQpaEu67zR1VVcXDiYjSFwzeBxNdTd0FMnSslLQXiRj8uGPzwsaoefrMD5XAmdw=="],
@@ -1797,7 +1842,7 @@
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
- "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+ "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
@@ -2041,6 +2086,8 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
+
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"simple-git": ["simple-git@3.28.0", "", { "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", "debug": "^4.4.0" } }, "sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w=="],
@@ -2083,11 +2130,13 @@
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
+ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
+
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
"statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="],
- "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
+ "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
"stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="],
@@ -2163,12 +2212,16 @@
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
+ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
+
"tinyclip": ["tinyclip@0.1.15", "", {}, "sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A=="],
"tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
+ "tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="],
+
"tmp": ["tmp@0.2.3", "", {}, "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w=="],
"tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="],
@@ -2279,6 +2332,8 @@
"vite-plugin-vue-tracer": ["vite-plugin-vue-tracer@1.4.0", "", { "dependencies": { "estree-walker": "^3.0.3", "exsolve": "^1.0.8", "magic-string": "^0.30.21", "pathe": "^2.0.3", "source-map-js": "^1.2.1" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.5.0" } }, "sha512-0tQCjCqZWVSK6UeRW9S4ABbf47lKQ68zvrT2FNvZmiL+alDydCVyH/T3Jlfbdc3T3C2Iuyyl5aVsMbF8IQIoxA=="],
+ "vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "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.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "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-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="],
+
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
"vue": ["vue@3.5.39", "", { "dependencies": { "@vue/compiler-dom": "3.5.39", "@vue/compiler-sfc": "3.5.39", "@vue/runtime-dom": "3.5.39", "@vue/server-renderer": "3.5.39", "@vue/shared": "3.5.39" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA=="],
@@ -2315,6 +2370,8 @@
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
+ "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=="],
+
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="],
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
@@ -2453,12 +2510,12 @@
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
+ "@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+
"@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
"@jridgewell/source-map/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
- "@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
-
"@mapbox/node-pre-gyp/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"@mapbox/node-pre-gyp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
@@ -2507,8 +2564,6 @@
"@nuxt/cli/perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="],
- "@nuxt/cli/std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
-
"@nuxt/cli/ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
"@nuxt/eslint/@nuxt/devtools-kit": ["@nuxt/devtools-kit@2.6.2", "", { "dependencies": { "@nuxt/kit": "^3.17.6", "execa": "^8.0.1" }, "peerDependencies": { "vite": ">=6.0" } }, "sha512-esErdMQ0u3wXXogKQ3IE2m0fxv52w6CzPsfsXF4o5ZVrUQrQaH58ygupDAQTYdlGTgtqmEA6KkHTGG5cM6yxeg=="],
@@ -2537,6 +2592,8 @@
"@nuxt/schema/defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="],
+ "@nuxt/schema/std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
+
"@nuxt/telemetry/ofetch": ["ofetch@2.0.0-alpha.3", "", {}, "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA=="],
"@nuxt/telemetry/std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="],
@@ -2557,16 +2614,18 @@
"@nuxt/vite-builder/knitwork": ["knitwork@1.3.0", "", {}, "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw=="],
- "@nuxt/vite-builder/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
-
"@nuxt/vite-builder/mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
"@nuxt/vite-builder/postcss": ["postcss@8.5.20", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug=="],
+ "@nuxt/vite-builder/std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
+
"@nuxt/vite-builder/ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
"@nuxtjs/i18n/@nuxt/kit": ["@nuxt/kit@4.0.2", "", { "dependencies": { "c12": "^3.1.0", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.7", "ignore": "^7.0.5", "jiti": "^2.5.1", "klona": "^2.0.6", "mlly": "^1.7.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.2.0", "scule": "^1.3.0", "semver": "^7.7.2", "std-env": "^3.9.0", "tinyglobby": "^0.2.14", "ufo": "^1.6.1", "unctx": "^2.4.1", "unimport": "^5.2.0", "untyped": "^2.0.0" } }, "sha512-OtLkVYHpfrm1FzGSGxl0H3QXLgO41yxOgni5S6zzLG4gblG71Fy82B2QTdqJLzTLKWObiILKDhrysBtmDkp3LA=="],
+ "@nuxtjs/i18n/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
+
"@nuxtjs/tailwindcss/@nuxt/kit": ["@nuxt/kit@3.18.0", "", { "dependencies": { "c12": "^3.1.0", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.7", "ignore": "^7.0.5", "jiti": "^2.5.1", "klona": "^2.0.6", "knitwork": "^1.2.0", "mlly": "^1.7.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.2.0", "scule": "^1.3.0", "semver": "^7.7.2", "std-env": "^3.9.0", "tinyglobby": "^0.2.14", "ufo": "^1.6.1", "unctx": "^2.4.1", "unimport": "^5.2.0", "untyped": "^2.0.0" } }, "sha512-svS1CBEx7gMgEIaNYrQt26J/t5bDSUdIf7GQWr5M6yszOzLw+IVzyfH7TBmuxZEbjovhLaJEG379mgKp82H/lA=="],
"@nuxtjs/tailwindcss/pkg-types": ["pkg-types@2.2.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ=="],
@@ -2579,6 +2638,8 @@
"@parcel/watcher-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="],
+ "@parcel/watcher-wasm/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"@pinia/nuxt/@nuxt/kit": ["@nuxt/kit@3.18.0", "", { "dependencies": { "c12": "^3.1.0", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.7", "ignore": "^7.0.5", "jiti": "^2.5.1", "klona": "^2.0.6", "knitwork": "^1.2.0", "mlly": "^1.7.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.2.0", "scule": "^1.3.0", "semver": "^7.7.2", "std-env": "^3.9.0", "tinyglobby": "^0.2.14", "ufo": "^1.6.1", "unctx": "^2.4.1", "unimport": "^5.2.0", "untyped": "^2.0.0" } }, "sha512-svS1CBEx7gMgEIaNYrQt26J/t5bDSUdIf7GQWr5M6yszOzLw+IVzyfH7TBmuxZEbjovhLaJEG379mgKp82H/lA=="],
"@poppinss/colors/kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
@@ -2595,11 +2656,11 @@
"@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
- "@rollup/plugin-commonjs/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+ "@rollup/plugin-commonjs/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"@rollup/plugin-inject/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
- "@rollup/plugin-replace/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+ "@rollup/plugin-inject/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
@@ -2645,6 +2706,8 @@
"@vercel/nft/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
+ "@vercel/nft/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"@vercel/nft/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
"@vue-macros/common/local-pkg": ["local-pkg@1.1.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.0.1", "quansync": "^0.2.8" } }, "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg=="],
@@ -2669,6 +2732,8 @@
"@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
+ "@vue/compiler-sfc/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
+
"@vue/compiler-ssr/@vue/compiler-dom": ["@vue/compiler-dom@3.5.18", "", { "dependencies": { "@vue/compiler-core": "3.5.18", "@vue/shared": "3.5.18" } }, "sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A=="],
"@vue/compiler-ssr/@vue/shared": ["@vue/shared@3.5.18", "", {}, "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA=="],
@@ -2743,6 +2808,8 @@
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
+ "effect/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
+
"escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"eslint/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
@@ -2821,6 +2888,10 @@
"local-pkg/mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
+ "magic-regexp/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
+
+ "magic-string-ast/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
+
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
@@ -2849,8 +2920,6 @@
"nitropack/knitwork": ["knitwork@1.3.0", "", {}, "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw=="],
- "nitropack/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
-
"nitropack/magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="],
"nitropack/mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
@@ -2901,14 +2970,14 @@
"nuxt/knitwork": ["knitwork@1.3.0", "", {}, "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw=="],
- "nuxt/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
-
"nuxt/mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
"nuxt/oxc-parser": ["oxc-parser@0.86.0", "", { "dependencies": { "@oxc-project/types": "^0.86.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm64": "0.86.0", "@oxc-parser/binding-darwin-arm64": "0.86.0", "@oxc-parser/binding-darwin-x64": "0.86.0", "@oxc-parser/binding-freebsd-x64": "0.86.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.86.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.86.0", "@oxc-parser/binding-linux-arm64-gnu": "0.86.0", "@oxc-parser/binding-linux-arm64-musl": "0.86.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.86.0", "@oxc-parser/binding-linux-s390x-gnu": "0.86.0", "@oxc-parser/binding-linux-x64-gnu": "0.86.0", "@oxc-parser/binding-linux-x64-musl": "0.86.0", "@oxc-parser/binding-wasm32-wasi": "0.86.0", "@oxc-parser/binding-win32-arm64-msvc": "0.86.0", "@oxc-parser/binding-win32-x64-msvc": "0.86.0" } }, "sha512-v9+uomgqyLSxlq3qlaMqJJtXg2+rUsa368p/zkmgi5OMGmcZAtZt5GIeSVFF84iNET+08Hdx/rUtd/FyIdfNFQ=="],
"nuxt/perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="],
+ "nuxt/std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
+
"nuxt/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"nuxt/ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
@@ -3081,12 +3150,18 @@
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
+ "tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
+ "unctx/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
+
"unhead/hookable": ["hookable@6.1.1", "", {}, "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ=="],
"unimport/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"unimport/local-pkg": ["local-pkg@1.1.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.0.1", "quansync": "^0.2.8" } }, "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg=="],
+ "unimport/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
+
"unimport/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"unimport/pkg-types": ["pkg-types@2.2.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ=="],
@@ -3101,8 +3176,12 @@
"unplugin/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+ "unplugin-utils/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"unplugin-vue-router/local-pkg": ["local-pkg@1.1.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.0.1", "quansync": "^0.2.8" } }, "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg=="],
+ "unplugin-vue-router/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
+
"unplugin-vue-router/unplugin-utils": ["unplugin-utils@0.2.4", "", { "dependencies": { "pathe": "^2.0.2", "picomatch": "^4.0.2" } }, "sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA=="],
"unstorage/ofetch": ["ofetch@1.4.1", "", { "dependencies": { "destr": "^2.0.3", "node-fetch-native": "^1.6.4", "ufo": "^1.5.4" } }, "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw=="],
@@ -3115,14 +3194,14 @@
"unwasm/knitwork": ["knitwork@1.3.0", "", {}, "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw=="],
- "unwasm/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
-
"unwasm/mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
"validate-npm-package-license/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="],
"vite/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
+ "vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"vite/postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="],
"vite-dev-rpc/birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="],
@@ -3135,12 +3214,8 @@
"vite-plugin-checker/npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
- "vite-plugin-checker/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
-
"vite-plugin-inspect/perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="],
- "vite-plugin-vue-tracer/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
-
"vue/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.39", "", { "dependencies": { "@babel/parser": "^7.29.7", "@vue/compiler-core": "3.5.39", "@vue/compiler-dom": "3.5.39", "@vue/compiler-ssr": "3.5.39", "@vue/shared": "3.5.39", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.15", "source-map-js": "^1.2.1" } }, "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg=="],
"vue-bundle-renderer/ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
@@ -3167,6 +3242,8 @@
"zip-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
+ "@ampproject/remapping/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+
"@babel/core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
"@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
@@ -3251,6 +3328,8 @@
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
+ "@jridgewell/source-map/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+
"@netlify/dev-utils/dot-prop/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"@netlify/zip-it-and-ship-it/@vercel/nft/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
@@ -3335,6 +3414,8 @@
"@nuxt/eslint/@nuxt/kit/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "@nuxt/eslint/@nuxt/kit/std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
+
"@nuxt/eslint/@nuxt/kit/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"@nuxt/kit/c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
@@ -3351,8 +3432,6 @@
"@nuxt/kit/unctx/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
- "@nuxt/kit/unctx/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
-
"@nuxt/kit/unctx/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=="],
"@nuxt/vite-builder/@nuxt/kit/c12": ["c12@3.3.4", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.4", "defu": "^6.1.6", "dotenv": "^17.3.1", "exsolve": "^1.0.8", "giget": "^3.2.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "pkg-types": "^2.3.0", "rc9": "^3.0.1" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA=="],
@@ -3425,8 +3504,6 @@
"@nuxt/vite-builder/h3/node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="],
- "@nuxt/vite-builder/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
-
"@nuxt/vite-builder/mlly/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
"@nuxt/vite-builder/mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
@@ -3441,14 +3518,20 @@
"@nuxtjs/i18n/@nuxt/kit/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "@nuxtjs/i18n/@nuxt/kit/std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
+
"@nuxtjs/i18n/@nuxt/kit/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
+ "@nuxtjs/i18n/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+
"@nuxtjs/tailwindcss/@nuxt/kit/exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
"@nuxtjs/tailwindcss/@nuxt/kit/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@nuxtjs/tailwindcss/@nuxt/kit/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "@nuxtjs/tailwindcss/@nuxt/kit/std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
+
"@nuxtjs/tailwindcss/@nuxt/kit/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"@nuxtjs/tailwindcss/pkg-types/confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
@@ -3475,6 +3558,8 @@
"@pinia/nuxt/@nuxt/kit/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "@pinia/nuxt/@nuxt/kit/std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
+
"@pinia/nuxt/@nuxt/kit/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"@prisma/config/c12/confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
@@ -3491,9 +3576,7 @@
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
- "@rollup/plugin-commonjs/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
-
- "@rollup/plugin-replace/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+ "@rollup/plugin-inject/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.40.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.40.0", "@typescript-eslint/types": "^8.40.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw=="],
@@ -3543,8 +3626,6 @@
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
- "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
-
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="],
"@vue/compiler-dom/@vue/compiler-core/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
@@ -3553,6 +3634,8 @@
"@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
+ "@vue/compiler-sfc/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+
"@vuestic/nuxt/@nuxt/kit/exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
"@vuestic/nuxt/@nuxt/kit/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
@@ -3563,6 +3646,8 @@
"@vuestic/nuxt/@nuxt/kit/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "@vuestic/nuxt/@nuxt/kit/std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
+
"@vuestic/nuxt/@nuxt/kit/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"@vuestic/nuxt/nuxt/@nuxt/cli": ["@nuxt/cli@3.27.0", "", { "dependencies": { "c12": "^3.1.0", "citty": "^0.1.6", "clipboardy": "^4.0.0", "confbox": "^0.2.2", "consola": "^3.4.2", "defu": "^6.1.4", "exsolve": "^1.0.7", "fuse.js": "^7.1.0", "get-port-please": "^3.2.0", "giget": "^2.0.0", "h3": "^1.15.3", "httpxy": "^0.1.7", "jiti": "^2.4.2", "listhen": "^1.9.0", "nypm": "^0.6.0", "ofetch": "^1.4.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "scule": "^1.3.0", "semver": "^7.7.2", "std-env": "^3.9.0", "tinyexec": "^1.0.1", "ufo": "^1.6.1", "youch": "^4.1.0-beta.10" }, "bin": { "nuxi": "bin/nuxi.mjs", "nuxi-ng": "bin/nuxi.mjs", "nuxt": "bin/nuxi.mjs", "nuxt-cli": "bin/nuxi.mjs" } }, "sha512-lOdzEvEbGaV06ebKKYgpumLLzbOZMFQzZfT4ZE7foa8/8aXG+GR3g8w9RX2IUyomTdSfapa3UcHDC8srQKRIEw=="],
@@ -3587,6 +3672,8 @@
"@vuestic/nuxt/nuxt/impound": ["impound@1.0.0", "", { "dependencies": { "exsolve": "^1.0.5", "mocked-exports": "^0.1.1", "pathe": "^2.0.3", "unplugin": "^2.3.2", "unplugin-utils": "^0.2.4" } }, "sha512-8lAJ+1Arw2sMaZ9HE2ZmL5zOcMnt18s6+7Xqgq2aUVy4P1nlzAyPtzCDxsk51KVFwHEEdc6OWvUyqwHwhRYaug=="],
+ "@vuestic/nuxt/nuxt/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
+
"@vuestic/nuxt/nuxt/nitropack": ["nitropack@2.12.4", "", { "dependencies": { "@cloudflare/kv-asset-handler": "^0.4.0", "@netlify/functions": "^3.1.10", "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^28.0.6", "@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-terser": "^0.4.4", "@vercel/nft": "^0.29.4", "archiver": "^7.0.1", "c12": "^3.1.0", "chokidar": "^4.0.3", "citty": "^0.1.6", "compatx": "^0.2.0", "confbox": "^0.2.2", "consola": "^3.4.2", "cookie-es": "^2.0.0", "croner": "^9.1.0", "crossws": "^0.3.5", "db0": "^0.3.2", "defu": "^6.1.4", "destr": "^2.0.5", "dot-prop": "^9.0.0", "esbuild": "^0.25.6", "escape-string-regexp": "^5.0.0", "etag": "^1.8.1", "exsolve": "^1.0.7", "globby": "^14.1.0", "gzip-size": "^7.0.0", "h3": "^1.15.3", "hookable": "^5.5.3", "httpxy": "^0.1.7", "ioredis": "^5.6.1", "jiti": "^2.4.2", "klona": "^2.0.6", "knitwork": "^1.2.0", "listhen": "^1.9.0", "magic-string": "^0.30.17", "magicast": "^0.3.5", "mime": "^4.0.7", "mlly": "^1.7.4", "node-fetch-native": "^1.6.6", "node-mock-http": "^1.0.1", "ofetch": "^1.4.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "pretty-bytes": "^6.1.1", "radix3": "^1.1.2", "rollup": "^4.45.0", "rollup-plugin-visualizer": "^6.0.3", "scule": "^1.3.0", "semver": "^7.7.2", "serve-placeholder": "^2.0.2", "serve-static": "^2.2.0", "source-map": "^0.7.4", "std-env": "^3.9.0", "ufo": "^1.6.1", "ultrahtml": "^1.6.0", "uncrypto": "^0.1.3", "unctx": "^2.4.1", "unenv": "^2.0.0-rc.18", "unimport": "^5.1.0", "unplugin-utils": "^0.2.4", "unstorage": "^1.16.1", "untyped": "^2.0.0", "unwasm": "^0.3.9", "youch": "4.1.0-beta.8", "youch-core": "^0.3.3" }, "peerDependencies": { "xml2js": "^0.6.2" }, "optionalPeers": ["xml2js"], "bin": { "nitro": "dist/cli/index.mjs", "nitropack": "dist/cli/index.mjs" } }, "sha512-MPmPRJWTeH03f/NmpN4q3iI3Woik4uaaWIoX34W3gMJiW06Vm1te/lPzuu5EXpXOK7Q2m3FymGMPXcExqih96Q=="],
"@vuestic/nuxt/nuxt/nypm": ["nypm@0.6.1", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.2.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w=="],
@@ -3605,6 +3692,8 @@
"@vuestic/nuxt/nuxt/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "@vuestic/nuxt/nuxt/std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
+
"@vuestic/nuxt/nuxt/strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
"@vuestic/nuxt/nuxt/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
@@ -3625,6 +3714,8 @@
"@vueuse/nuxt/@nuxt/kit/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "@vueuse/nuxt/@nuxt/kit/std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
+
"@vueuse/nuxt/@nuxt/kit/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"@vueuse/nuxt/local-pkg/pkg-types": ["pkg-types@2.2.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ=="],
@@ -3673,6 +3764,8 @@
"http-assert/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
+ "impound/unplugin/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"koa-send/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
"koa/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
@@ -3711,6 +3804,8 @@
"listhen/@parcel/watcher/node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
+ "listhen/@parcel/watcher/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"listhen/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="],
"listhen/h3/node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="],
@@ -3725,6 +3820,10 @@
"local-pkg/mlly/ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
+ "magic-regexp/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+
+ "magic-string-ast/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"nitropack/c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
@@ -3793,8 +3892,6 @@
"nitropack/ioredis/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
- "nitropack/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
-
"nitropack/magicast/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
"nitropack/magicast/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
@@ -3803,6 +3900,8 @@
"nitropack/mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
+ "nitropack/rollup-plugin-visualizer/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"nitropack/rollup-plugin-visualizer/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
"nitropack/unctx/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
@@ -3811,6 +3910,8 @@
"nitropack/unimport/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
+ "nitropack/unimport/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"nitropack/unimport/unplugin": ["unplugin@3.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.4", "webpack-virtual-modules": "^0.6.2" }, "peerDependencies": { "@farmfe/core": "*", "@rspack/core": "*", "bun-types-no-globals": "*", "esbuild": "*", "rolldown": "*", "rollup": "*", "unloader": "*", "vite": "*", "webpack": "*" }, "optionalPeers": ["@farmfe/core", "@rspack/core", "bun-types-no-globals", "esbuild", "rolldown", "rollup", "unloader", "vite", "webpack"] }, "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg=="],
"nitropack/unstorage/lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="],
@@ -3823,6 +3924,8 @@
"nuxt-auth-utils/@nuxt/kit/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "nuxt-auth-utils/@nuxt/kit/std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
+
"nuxt-auth-utils/@nuxt/kit/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"nuxt/@nuxt/kit/rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
@@ -3891,8 +3994,6 @@
"nuxt/h3/node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="],
- "nuxt/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
-
"nuxt/mlly/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
"nuxt/mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
@@ -3935,12 +4036,12 @@
"nuxt/unplugin/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
+ "nuxt/unplugin/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"nuxt/unplugin-vue-router/@vue-macros/common": ["@vue-macros/common@3.0.0-beta.16", "", { "dependencies": { "@vue/compiler-sfc": "^3.5.17", "ast-kit": "^2.1.1", "local-pkg": "^1.1.1", "magic-string-ast": "^1.0.0", "unplugin-utils": "^0.2.4" }, "peerDependencies": { "vue": "^2.7.0 || ^3.2.25" }, "optionalPeers": ["vue"] }, "sha512-8O2gWxWFiaoNkk7PGi0+p7NPGe/f8xJ3/INUufvje/RZOs7sJvlI1jnR4lydtRFa/mU0ylMXUXXjSK0fHDEYTA=="],
"nuxt/unplugin-vue-router/ast-walker-scope": ["ast-walker-scope@0.8.1", "", { "dependencies": { "@babel/parser": "^7.27.2", "ast-kit": "^2.0.0" } }, "sha512-72XOdbzQCMKERvFrxAykatn2pu7osPNq/sNUzwcHdWzwPvOsNpPqkawfDXVvQbA2RT+ivtsMNjYdojTUZitt1A=="],
- "nuxt/unplugin-vue-router/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
-
"nuxt/unplugin-vue-router/tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"nuxt/unplugin-vue-router/unplugin-utils": ["unplugin-utils@0.2.4", "", { "dependencies": { "pathe": "^2.0.2", "picomatch": "^4.0.2" } }, "sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA=="],
@@ -4095,8 +4196,12 @@
"tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
+ "unctx/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+
"unimport/local-pkg/quansync": ["quansync@0.2.10", "", {}, "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A=="],
+ "unimport/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+
"unimport/pkg-types/confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
"unimport/pkg-types/exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
@@ -4107,9 +4212,9 @@
"unplugin-vue-router/local-pkg/quansync": ["quansync@0.2.10", "", {}, "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A=="],
- "unplugin-vue-router/unplugin-utils/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+ "unplugin-vue-router/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
- "unwasm/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+ "unplugin-vue-router/unplugin-utils/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"unwasm/mlly/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
@@ -4129,8 +4234,6 @@
"vite-plugin-checker/npm-run-path/unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
- "vite-plugin-vue-tracer/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
-
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
"vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
@@ -4193,8 +4296,6 @@
"vue/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
- "vue/@vue/compiler-sfc/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
-
"vue/@vue/compiler-sfc/postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="],
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@@ -4231,6 +4332,8 @@
"@nuxt/eslint/@nuxt/devtools-kit/@nuxt/kit/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "@nuxt/eslint/@nuxt/devtools-kit/@nuxt/kit/std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
+
"@nuxt/eslint/@nuxt/devtools-kit/@nuxt/kit/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"@nuxt/eslint/@nuxt/kit/pkg-types/confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
@@ -4245,7 +4348,7 @@
"@nuxt/kit/mlly/pkg-types/mlly": ["mlly@1.7.4", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw=="],
- "@nuxt/kit/unctx/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+ "@nuxt/kit/unctx/unplugin/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"@nuxt/vite-builder/@nuxt/kit/c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
@@ -4313,8 +4416,6 @@
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
- "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
-
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
"@vue/compiler-dom/@vue/compiler-core/@babel/parser/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
@@ -4385,6 +4486,8 @@
"@vuestic/nuxt/nuxt/impound/unplugin-utils": ["unplugin-utils@0.2.4", "", { "dependencies": { "pathe": "^2.0.2", "picomatch": "^4.0.2" } }, "sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA=="],
+ "@vuestic/nuxt/nuxt/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+
"@vuestic/nuxt/nuxt/nitropack/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="],
"@vuestic/nuxt/nuxt/nitropack/@rollup/plugin-alias": ["@rollup/plugin-alias@5.1.1", "", { "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ=="],
@@ -4587,6 +4690,8 @@
"nitropack/rollup-plugin-visualizer/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
+ "nitropack/unctx/unplugin/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"nuxt-auth-utils/@nuxt/kit/pkg-types/confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
"nuxt-auth-utils/@nuxt/kit/tinyglobby/fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
@@ -4595,6 +4700,8 @@
"nuxt/@nuxt/kit/rc9/defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
+ "nuxt/@nuxt/kit/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"nuxt/c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"nuxt/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
@@ -4671,10 +4778,10 @@
"vue/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
- "vue/@vue/compiler-sfc/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
-
"vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
+ "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+
"@nuxt/eslint/@nuxt/devtools-kit/@nuxt/kit/pkg-types/confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
"@nuxt/eslint/@nuxt/devtools-kit/@nuxt/kit/tinyglobby/fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
@@ -4687,6 +4794,8 @@
"@nuxt/vite-builder/@nuxt/kit/c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
+ "@nuxt/vite-builder/@nuxt/kit/unctx/unplugin/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"@nuxt/vite-builder/mlly/pkg-types/mlly/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"@nuxt/vite-builder/mlly/pkg-types/mlly/ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
@@ -5125,6 +5234,8 @@
"nuxt/unplugin-vue-router/@vue-macros/common/ast-kit/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
+ "@vuestic/nuxt/nuxt/@nuxt/vite-builder/@vitejs/plugin-vue-jsx/@babel/core/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+
"@vuestic/nuxt/nuxt/@nuxt/vite-builder/@vitejs/plugin-vue-jsx/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@vuestic/nuxt/nuxt/@nuxt/vite-builder/@vitejs/plugin-vue-jsx/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/traverse/@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=="],
@@ -5160,5 +5271,11 @@
"@vuestic/nuxt/nuxt/@nuxt/vite-builder/@vitejs/plugin-vue-jsx/@babel/plugin-transform-typescript/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"@vuestic/nuxt/nuxt/@nuxt/vite-builder/@vitejs/plugin-vue-jsx/@babel/plugin-transform-typescript/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
+
+ "@vuestic/nuxt/nuxt/@nuxt/vite-builder/@vitejs/plugin-vue-jsx/@vue/babel-plugin-jsx/@babel/traverse/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+
+ "@vuestic/nuxt/nuxt/@nuxt/vite-builder/@vitejs/plugin-vue-jsx/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+
+ "@vuestic/nuxt/nuxt/@nuxt/vite-builder/@vitejs/plugin-vue-jsx/@babel/plugin-transform-typescript/@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
}
}
diff --git a/package.json b/package.json
index 9a4f8e5..c5b1045 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"format": "prettier --write .",
"check": "prettier --check .",
"test": "bun test",
+ "test:integration": "vitest run -c vitest.integration.config.ts",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "nuxt typecheck",
@@ -46,12 +47,15 @@
"zod": "^4.4.3"
},
"devDependencies": {
+ "@electric-sql/pglite": "^0.5.5",
+ "@electric-sql/pglite-socket": "^0.2.8",
"@nuxt/devtools": "^2.6.4",
"@types/bun": "^1.2.20",
"prettier": "^3.6.2",
"prettier-plugin-organize-imports": "^4.2.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"prisma": "^6.16.1",
+ "vitest": "^4.1.10",
"vue-tsc": "^3.0.6"
},
"patchedDependencies": {
diff --git a/tests/helpers/api.ts b/tests/helpers/api.ts
new file mode 100644
index 0000000..1258091
--- /dev/null
+++ b/tests/helpers/api.ts
@@ -0,0 +1,69 @@
+import { baseUrl } from '../integration/setup';
+
+/**
+ * Minimal fetch wrapper bound to the test server with a per-instance
+ * cookie jar, so each client represents one browser session.
+ */
+export class ApiClient {
+ private readonly cookies = new Map();
+
+ constructor(private readonly base: string) {}
+
+ async request(path: string, init: RequestInit = {}): Promise {
+ const headers = new Headers(init.headers);
+ if (this.cookies.size > 0) {
+ headers.set(
+ 'cookie',
+ [...this.cookies].map(([name, value]) => `${name}=${value}`).join('; ')
+ );
+ }
+
+ const res = await fetch(`${this.base}${path}`, { ...init, headers });
+
+ for (const cookie of res.headers.getSetCookie()) {
+ const [pair, ...attributes] = cookie.split(';');
+ const separator = pair.indexOf('=');
+ const name = pair.slice(0, separator).trim();
+ const value = pair.slice(separator + 1).trim();
+ const expired = attributes.some((attr) =>
+ /^expires=Thu, 01 Jan 1970/i.test(attr.trim())
+ );
+ if (value === '' || expired) {
+ this.cookies.delete(name);
+ } else {
+ this.cookies.set(name, value);
+ }
+ }
+ return res;
+ }
+
+ get(path: string) {
+ return this.request(path);
+ }
+
+ post(path: string, body?: unknown) {
+ const init: RequestInit =
+ body instanceof FormData
+ ? { method: 'POST', body }
+ : {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify(body ?? {})
+ };
+ return this.request(path, init);
+ }
+
+ async register(email: string, name: string, password: string) {
+ return this.post('/api/auth/register', { email, name, password });
+ }
+
+ async login(email: string, password: string) {
+ return this.post('/api/auth/login', { email, password });
+ }
+}
+
+export const api = new ApiClient(baseUrl);
+
+export function newClient() {
+ return new ApiClient(baseUrl);
+}
diff --git a/tests/helpers/database.ts b/tests/helpers/database.ts
new file mode 100644
index 0000000..14a872e
--- /dev/null
+++ b/tests/helpers/database.ts
@@ -0,0 +1,19 @@
+import { prisma } from '../integration/setup';
+
+/**
+ * Truncates every table in the public schema (except _prisma_migrations)
+ * so each test starts from an empty database. Sequences are reset to keep
+ * autoincrement ids predictable.
+ */
+export async function resetDatabase() {
+ const tables = await prisma.$queryRawUnsafe<{ tablename: string }[]>(
+ `SELECT tablename::text AS tablename FROM pg_tables
+ WHERE schemaname = 'public' AND tablename <> '_prisma_migrations'`
+ );
+ if (tables.length === 0) return;
+
+ const list = tables.map(({ tablename }) => `"public"."${tablename}"`);
+ await prisma.$executeRawUnsafe(
+ `TRUNCATE ${list.join(', ')} RESTART IDENTITY CASCADE;`
+ );
+}
diff --git a/tests/helpers/factories.ts b/tests/helpers/factories.ts
new file mode 100644
index 0000000..352b671
--- /dev/null
+++ b/tests/helpers/factories.ts
@@ -0,0 +1,147 @@
+import type { Prisma, UserRole } from '#shared/generated/prisma/client';
+import { hash } from 'argon2';
+import { randomBytes, randomInt } from 'node:crypto';
+import { prisma } from '../integration/setup';
+
+/** Default plaintext password used by createUser(); login tests rely on it. */
+export const TEST_PASSWORD = 'super-secret-123';
+
+let seq = 0;
+
+// argon2 hashing is comparatively slow, so cache one hash per password.
+const passwordHashes = new Map>();
+function hashPassword(password: string) {
+ if (!passwordHashes.has(password)) {
+ passwordHashes.set(password, hash(password));
+ }
+ return passwordHashes.get(password)!;
+}
+
+export async function createUser(
+ overrides: {
+ email?: string;
+ name?: string;
+ password?: string;
+ role?: UserRole;
+ } = {}
+) {
+ seq += 1;
+ return prisma.user.create({
+ data: {
+ email: overrides.email ?? `user-${seq}-${randomInt(100_000)}@test.dev`,
+ name: overrides.name ?? `User ${seq}`,
+ password: await hashPassword(overrides.password ?? TEST_PASSWORD),
+ role: overrides.role
+ }
+ });
+}
+
+export async function createHunt(
+ creatorId: number,
+ overrides: Partial = {}
+) {
+ seq += 1;
+ return prisma.hunt.create({
+ data: {
+ name_en: `Hunt ${seq}`,
+ name_de: `Jagd ${seq}`,
+ description_en: `Description ${seq}`,
+ description_de: `Beschreibung ${seq}`,
+ creatorId,
+ ...overrides
+ }
+ });
+}
+
+export async function createHuntMember(huntId: number, memberId: number) {
+ return prisma.huntMember.create({
+ data: { huntId, memberId }
+ });
+}
+
+export async function createQuest(
+ huntId: number,
+ overrides: Partial = {}
+) {
+ seq += 1;
+ return prisma.huntQuest.create({
+ data: {
+ title_en: `Quest ${seq}`,
+ title_de: `Aufgabe ${seq}`,
+ huntId,
+ ...overrides
+ }
+ });
+}
+
+export async function createTeam(
+ huntId: number,
+ ownerId: number,
+ overrides: Partial = {}
+) {
+ seq += 1;
+ return prisma.huntTeam.create({
+ data: {
+ name: `Team ${seq}`,
+ // same format as the production endpoint: 6 hex chars
+ password: randomBytes(3).toString('hex'),
+ huntId,
+ ownerId,
+ ...overrides
+ }
+ });
+}
+
+export async function createTeamMember(teamId: number, memberId: number) {
+ return prisma.teamMember.create({
+ data: { teamId, memberId }
+ });
+}
+
+export async function createAnswer(
+ {
+ teamId,
+ questId,
+ memberId
+ }: { teamId: number; questId: number; memberId: number },
+ overrides: Partial = {}
+) {
+ return prisma.questAnswer.create({
+ data: {
+ teamId,
+ questId,
+ memberId,
+ ...overrides
+ }
+ });
+}
+
+export async function createFile(
+ creatorId: number,
+ overrides: Partial = {}
+) {
+ seq += 1;
+ const uuid = `file-${seq}-${randomInt(100_000)}`;
+ return prisma.file.create({
+ data: {
+ uuid,
+ url: `https://bucket.test/${uuid}`,
+ creatorId,
+ ...overrides
+ }
+ });
+}
+
+export async function createReview(
+ answerId: number,
+ reviewerId: number,
+ overrides: Partial = {}
+) {
+ return prisma.review.create({
+ data: {
+ answerId,
+ reviewerId,
+ ...overrides
+ }
+ });
+}
diff --git a/tests/integration/auth.itest.ts b/tests/integration/auth.itest.ts
new file mode 100644
index 0000000..f2cebf4
--- /dev/null
+++ b/tests/integration/auth.itest.ts
@@ -0,0 +1,124 @@
+import { beforeEach, describe, expect, it } from 'vitest';
+import { newClient } from '../helpers/api';
+import { resetDatabase } from '../helpers/database';
+import { createUser, TEST_PASSWORD } from '../helpers/factories';
+
+beforeEach(resetDatabase);
+
+describe('register', () => {
+ it('creates a user and starts a session', async () => {
+ const client = newClient();
+ const res = await client.register(
+ 'new-user@test.dev',
+ 'New User',
+ 'verysecret1'
+ );
+ expect(res.status).toBe(200);
+ const body = await res.json();
+ expect(body.success).toBe(true);
+ expect(body.user.id).toBeTypeOf('number');
+
+ // The register response already carries a session cookie.
+ const profile = await client.get('/api/auth/profile');
+ expect(profile.status).toBe(200);
+ expect(await profile.json()).toMatchObject({
+ name: 'New User',
+ email: 'new-user@test.dev',
+ uploadedFiles: []
+ });
+ });
+
+ it('rejects invalid input with 400', async () => {
+ const client = newClient();
+ // password too short
+ expect(
+ (await client.register('short@test.dev', 'Short', 'short')).status
+ ).toBe(400);
+ // invalid email
+ expect(
+ (await client.register('not-an-email', 'NoMail', 'longenough1')).status
+ ).toBe(400);
+ // empty name
+ expect(
+ (await client.register('empty@test.dev', '', 'longenough1')).status
+ ).toBe(400);
+ });
+
+ it('rejects duplicate email with 409', async () => {
+ await createUser({ email: 'dupe@test.dev' });
+ const client = newClient();
+ const res = await client.register('dupe@test.dev', 'Dupe', 'verysecret1');
+ expect(res.status).toBe(409);
+ });
+});
+
+describe('login', () => {
+ it('logs in with correct credentials', async () => {
+ const user = await createUser({ email: 'login@test.dev' });
+ const client = newClient();
+ const res = await client.login('login@test.dev', TEST_PASSWORD);
+ expect(res.status).toBe(200);
+ expect(await res.json()).toEqual({
+ success: true,
+ user: { id: user.id }
+ });
+
+ const profile = await client.get('/api/auth/profile');
+ expect(profile.status).toBe(200);
+ });
+
+ it('rejects wrong password and unknown email with 404', async () => {
+ await createUser({ email: 'known@test.dev' });
+ const client = newClient();
+ expect(
+ (await client.login('known@test.dev', 'wrong-password-1')).status
+ ).toBe(404);
+ expect((await client.login('unknown@test.dev', TEST_PASSWORD)).status).toBe(
+ 404
+ );
+ });
+});
+
+describe('profile', () => {
+ it('requires a session', async () => {
+ const res = await newClient().get('/api/auth/profile');
+ expect(res.status).toBe(401);
+ });
+
+ it('updates name and email', async () => {
+ await createUser({ email: 'update-me@test.dev' });
+ const client = newClient();
+ await client.login('update-me@test.dev', TEST_PASSWORD);
+
+ const res = await client.post('/api/auth/profile', {
+ name: 'Updated Name',
+ email: 'updated@test.dev'
+ });
+ expect(res.status).toBe(200);
+ expect(await res.json()).toEqual({ ok: true });
+
+ const profile = await client.get('/api/auth/profile');
+ expect(await profile.json()).toMatchObject({
+ name: 'Updated Name',
+ email: 'updated@test.dev'
+ });
+
+ // login works with the new email afterwards
+ expect(
+ (await newClient().login('updated@test.dev', TEST_PASSWORD)).status
+ ).toBe(200);
+ });
+
+ it('rejects taking an existing email with 409', async () => {
+ await createUser({ email: 'first@test.dev' });
+ await createUser({ email: 'second@test.dev' });
+ const client = newClient();
+ await client.login('first@test.dev', TEST_PASSWORD);
+
+ const res = await client.post('/api/auth/profile', {
+ name: 'First User',
+ email: 'second@test.dev'
+ });
+ expect(res.status).toBe(409);
+ });
+});
diff --git a/tests/integration/diashow.itest.ts b/tests/integration/diashow.itest.ts
new file mode 100644
index 0000000..bec3ec3
--- /dev/null
+++ b/tests/integration/diashow.itest.ts
@@ -0,0 +1,137 @@
+import { beforeEach, describe, expect, it } from 'vitest';
+import { newClient } from '../helpers/api';
+import { resetDatabase } from '../helpers/database';
+import {
+ createAnswer,
+ createFile,
+ createHunt,
+ createQuest,
+ createTeam,
+ createUser,
+ TEST_PASSWORD
+} from '../helpers/factories';
+import { prisma } from './setup';
+
+beforeEach(resetDatabase);
+
+/**
+ * Hunt with one quest and three finalized answers:
+ * - a public picture (shown)
+ * - a private picture (hidden)
+ * - no picture at all (hidden)
+ */
+async function seedDiashow(reveal: boolean) {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id, {
+ revealAnswers: reveal,
+ revealQuests: reveal
+ });
+ const quest = await createQuest(hunt.id);
+ const owner = await createUser();
+ const team = await createTeam(hunt.id, owner.id);
+
+ const answerPublic = await createAnswer({
+ teamId: team.id,
+ questId: quest.id,
+ memberId: owner.id
+ });
+ const publicFile = await createFile(owner.id, { answerId: answerPublic.id });
+ await prisma.questAnswer.update({
+ where: { id: answerPublic.id },
+ data: { pictureId: publicFile.id, final: true }
+ });
+
+ const answerPrivate = await createAnswer({
+ teamId: team.id,
+ questId: quest.id,
+ memberId: owner.id
+ });
+ const privateFile = await createFile(owner.id, {
+ answerId: answerPrivate.id,
+ private: true
+ });
+ await prisma.questAnswer.update({
+ where: { id: answerPrivate.id },
+ data: { pictureId: privateFile.id, final: true }
+ });
+
+ await createAnswer(
+ { teamId: team.id, questId: quest.id, memberId: owner.id },
+ { final: true, text: 'no picture' }
+ );
+
+ return { hunt, quest, team, publicFile };
+}
+
+describe('diashow (GET /api/hunt/[huntId]/diashow)', () => {
+ it('returns 404 for unknown hunts', async () => {
+ const res = await newClient().get('/api/hunt/999999/diashow');
+ expect(res.status).toBe(404);
+ });
+
+ it('blocks anonymous users before answers are revealed', async () => {
+ const { hunt } = await seedDiashow(false);
+ const res = await newClient().get(`/api/hunt/${hunt.id}/diashow`);
+ expect(res.status).toBe(403);
+ });
+
+ it('lets the hunt creator peek before reveal', async () => {
+ const { hunt } = await seedDiashow(false);
+ const creator = await prisma.user.findFirstOrThrow({
+ where: { huntsCreated: { some: { id: hunt.id } } }
+ });
+
+ const client = newClient();
+ await client.login(creator.email, TEST_PASSWORD);
+ const res = await client.get(`/api/hunt/${hunt.id}/diashow`);
+ expect(res.status).toBe(200);
+ });
+
+ it('only shows finalized answers with public pictures', async () => {
+ const { hunt, quest, team, publicFile } = await seedDiashow(true);
+
+ const res = await newClient().get(`/api/hunt/${hunt.id}/diashow`);
+ expect(res.status).toBe(200);
+ const body = await res.json();
+
+ expect(body.id).toBe(hunt.id);
+ expect(body.quests).toHaveLength(1);
+ expect(body.quests[0].id).toBe(quest.id);
+ expect(body.quests[0].answers).toHaveLength(1);
+ expect(body.quests[0].answers[0].picture.url).toBe(publicFile.url);
+ expect(body.quests[0].answers[0].team).toMatchObject({
+ id: team.id,
+ name: team.name
+ });
+ });
+
+ it('omits quests whose final answers are all private', async () => {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id, {
+ revealAnswers: true,
+ revealQuests: true
+ });
+ const quest = await createQuest(hunt.id);
+ const owner = await createUser();
+ const team = await createTeam(hunt.id, owner.id);
+
+ const answer = await createAnswer({
+ teamId: team.id,
+ questId: quest.id,
+ memberId: owner.id
+ });
+ const privateFile = await createFile(owner.id, {
+ answerId: answer.id,
+ private: true
+ });
+ await prisma.questAnswer.update({
+ where: { id: answer.id },
+ data: { pictureId: privateFile.id, final: true }
+ });
+
+ const res = await newClient().get(`/api/hunt/${hunt.id}/diashow`);
+ expect(res.status).toBe(200);
+ const body = await res.json();
+ expect(body.quests).toEqual([]);
+ });
+});
diff --git a/tests/integration/global-setup.ts b/tests/integration/global-setup.ts
new file mode 100644
index 0000000..6e3548f
--- /dev/null
+++ b/tests/integration/global-setup.ts
@@ -0,0 +1,140 @@
+import { spawn, type ChildProcess } from 'node:child_process';
+import { createServer } from 'node:net';
+import type { TestProject } from 'vitest/node';
+
+// Fixed test-only secret, >= 32 chars as required by nuxt-auth-utils.
+const SESSION_PASSWORD = 'integration-tests-only-not-for-prod-use!';
+
+async function freePort(): Promise {
+ return new Promise((resolve, reject) => {
+ const server = createServer();
+ server.listen(0, '127.0.0.1', () => {
+ const address = server.address();
+ if (address === null || typeof address === 'string') {
+ reject(new Error('Could not determine a free port'));
+ return;
+ }
+ server.close(() => resolve(address.port));
+ });
+ });
+}
+
+function waitForLine(child: ChildProcess, pattern: RegExp): Promise {
+ return new Promise((resolve, reject) => {
+ let buffer = '';
+ const onData = (chunk: Buffer) => {
+ buffer += chunk.toString();
+ for (const line of buffer.split('\n')) {
+ const match = line.match(pattern);
+ if (match) {
+ child.stdout?.off('data', onData);
+ resolve(match.slice(1));
+ return;
+ }
+ }
+ };
+ child.stdout?.on('data', onData);
+ child.on('exit', (code) =>
+ reject(new Error(`Child exited with code ${code} before being ready`))
+ );
+ child.on('error', reject);
+ });
+}
+
+function run(command: string, args: string[], env: NodeJS.ProcessEnv = {}) {
+ return new Promise((resolve, reject) => {
+ const child = spawn(command, args, {
+ stdio: 'inherit',
+ env: { ...process.env, ...env }
+ });
+ child.on('exit', (code) =>
+ code === 0
+ ? resolve()
+ : reject(new Error(`${command} ${args.join(' ')} exited with ${code}`))
+ );
+ child.on('error', reject);
+ });
+}
+
+async function waitForHealth(baseUrl: string, timeoutMs = 60_000) {
+ const deadline = Date.now() + timeoutMs;
+ let lastError: unknown = new Error('server never became healthy');
+ while (Date.now() < deadline) {
+ try {
+ const res = await fetch(`${baseUrl}/api/health`);
+ if (res.ok) return;
+ lastError = new Error(`health responded with ${res.status}`);
+ } catch (e) {
+ lastError = e;
+ }
+ await new Promise((r) => setTimeout(r, 250));
+ }
+ throw lastError;
+}
+
+function kill(child: ChildProcess) {
+ return new Promise((resolve) => {
+ if (child.exitCode !== null) return resolve();
+ const timer = setTimeout(() => {
+ child.kill('SIGKILL');
+ }, 3_000);
+ child.on('exit', () => {
+ clearTimeout(timer);
+ resolve();
+ });
+ child.kill('SIGTERM');
+ });
+}
+
+export default async function globalSetup(project: TestProject) {
+ // 1. Boot in-memory PGlite behind a Postgres wire protocol socket.
+ // It runs in a child process: teardown under Bun exits with code 99
+ // (Emscripten), and isolating it keeps the vitest process clean.
+ const dbHost = spawn('node', ['tests/integration/pglite-host.mjs'], {
+ stdio: ['ignore', 'pipe', 'inherit']
+ });
+ const [databaseUrl] = await waitForLine(dbHost, /^PGLITE_READY (.+)$/);
+
+ // 2. Apply all migrations against the socket (proven path from Phase 0).
+ await run('bunx', ['prisma', 'migrate', 'deploy'], {
+ DATABASE_URL: databaseUrl
+ });
+
+ // 3. Build the Nuxt server once for all test files.
+ if (process.env.SKIP_BUILD !== '1') {
+ await run('bun', ['run', 'build']);
+ }
+
+ // 4. Spawn the built server against the WASM database.
+ const port = await freePort();
+ const baseUrl = `http://127.0.0.1:${port}`;
+ const server = spawn('node', ['.output/server/index.mjs'], {
+ stdio: 'inherit',
+ env: {
+ ...process.env,
+ NODE_ENV: 'production',
+ PORT: String(port),
+ DATABASE_URL: databaseUrl,
+ NUXT_SESSION_PASSWORD: SESSION_PASSWORD
+ }
+ });
+ server.on('error', (e) => {
+ throw e;
+ });
+
+ try {
+ await waitForHealth(baseUrl);
+ } catch (e) {
+ await kill(server);
+ await kill(dbHost);
+ throw e;
+ }
+
+ project.provide('baseUrl', baseUrl);
+ project.provide('databaseUrl', databaseUrl);
+
+ return async () => {
+ await kill(server);
+ await kill(dbHost);
+ };
+}
diff --git a/tests/integration/health.itest.ts b/tests/integration/health.itest.ts
new file mode 100644
index 0000000..9a947b6
--- /dev/null
+++ b/tests/integration/health.itest.ts
@@ -0,0 +1,11 @@
+import { describe, expect, it } from 'vitest';
+import { api } from '../helpers/api';
+
+// Proves the full loop: PGlite -> migrations -> build -> server -> fetch.
+describe('health', () => {
+ it('reports a working database connection', async () => {
+ const res = await api.get('/api/health');
+ expect(res.status).toBe(200);
+ expect(await res.json()).toEqual({ ok: true, db: true });
+ });
+});
diff --git a/tests/integration/hunt.itest.ts b/tests/integration/hunt.itest.ts
new file mode 100644
index 0000000..55fc852
--- /dev/null
+++ b/tests/integration/hunt.itest.ts
@@ -0,0 +1,183 @@
+import { beforeEach, describe, expect, it } from 'vitest';
+import { newClient } from '../helpers/api';
+import { resetDatabase } from '../helpers/database';
+import {
+ createHunt,
+ createHuntMember,
+ createQuest,
+ createTeam,
+ createTeamMember,
+ createUser,
+ TEST_PASSWORD
+} from '../helpers/factories';
+import { prisma } from './setup';
+
+beforeEach(resetDatabase);
+
+describe('hunt listing (GET /api/home)', () => {
+ it('lists only public, joinable hunts that have not started yet', async () => {
+ const creator = await createUser();
+ await createHunt(creator.id, { allowJoin: true });
+ await createHunt(creator.id, { allowJoin: true, public: false });
+ await createHunt(creator.id, { allowJoin: false });
+ await createHunt(creator.id, {
+ allowJoin: true,
+ start: new Date('2020-01-01')
+ });
+ await createHunt(creator.id, {
+ allowJoin: true,
+ start: new Date(Date.now() + 24 * 60 * 60 * 1000)
+ });
+ await createHunt(creator.id, {
+ allowJoin: true,
+ deletedAt: new Date()
+ });
+
+ const res = await newClient().get('/api/home');
+ expect(res.status).toBe(200);
+ const body = await res.json();
+ expect(body.own).toBeNull();
+ // one hunt without start + one future hunt
+ expect(body.others).toHaveLength(2);
+ });
+
+ it('lists hunts the user participates in under own', async () => {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id, { allowJoin: true });
+ const player = await createUser({ email: 'player@test.dev' });
+ const team = await createTeam(hunt.id, creator.id);
+ await createTeamMember(team.id, player.id);
+
+ const client = newClient();
+ await client.login('player@test.dev', TEST_PASSWORD);
+ const res = await client.get('/api/home');
+ const body = await res.json();
+
+ expect(body.own).toHaveLength(1);
+ expect(body.own[0].id).toBe(hunt.id);
+ // the hunt is excluded from others because the user already plays it
+ expect(body.others).toHaveLength(0);
+ });
+});
+
+describe('hunt detail (GET /api/hunt/[huntId])', () => {
+ it('returns 404 for unknown hunts', async () => {
+ const res = await newClient().get('/api/hunt/999999');
+ expect(res.status).toBe(404);
+ });
+
+ it('hides quests from anonymous users when revealQuests is false', async () => {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id);
+ await createQuest(hunt.id);
+
+ const res = await newClient().get(`/api/hunt/${hunt.id}`);
+ expect(res.status).toBe(200);
+ const body = await res.json();
+ expect(body.quests).toBeNull();
+ expect(body.isMember).toBe(false);
+ expect(body.loggedIn).toBe(false);
+ expect(body.ownTeam).toBeNull();
+ });
+
+ it('shows quests to hunt members even without revealQuests', async () => {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id);
+ await createQuest(hunt.id);
+ const member = await createUser({ email: 'member@test.dev' });
+ await createHuntMember(hunt.id, member.id);
+
+ const client = newClient();
+ await client.login('member@test.dev', TEST_PASSWORD);
+ const res = await client.get(`/api/hunt/${hunt.id}`);
+ const body = await res.json();
+ expect(body.isMember).toBe(true);
+ expect(body.quests).toHaveLength(1);
+ });
+
+ it('exposes the own team to team owners', async () => {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id, { revealQuests: true });
+ const team = await createTeam(hunt.id, creator.id, { name: 'Owners' });
+
+ const client = newClient();
+ await client.login(creator.email, TEST_PASSWORD);
+ const res = await client.get(`/api/hunt/${hunt.id}`);
+ const body = await res.json();
+ expect(body.ownTeam).toMatchObject({ id: team.id, name: 'Owners' });
+ });
+});
+
+describe('join hunt (POST /api/hunt/[huntId]/join)', () => {
+ it('requires a session', async () => {
+ const res = await newClient().post('/api/hunt/1/join', {
+ team: 1,
+ password: 'x'
+ });
+ expect(res.status).toBe(401);
+ });
+
+ it('joins a team with the correct invite password', async () => {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id, { allowJoin: true });
+ const team = await createTeam(hunt.id, creator.id, { password: 'abc123' });
+ const player = await createUser({ email: 'joiner@test.dev' });
+
+ const client = newClient();
+ await client.login('joiner@test.dev', TEST_PASSWORD);
+ const res = await client.post(`/api/hunt/${hunt.id}/join`, {
+ team: team.id,
+ password: 'abc123'
+ });
+ expect(res.status).toBe(200);
+ expect(await res.json()).toEqual({ success: true });
+
+ expect(
+ await prisma.teamMember.count({
+ where: { teamId: team.id, memberId: player.id }
+ })
+ ).toBe(1);
+ });
+
+ it('rejects wrong team or password with 404', async () => {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id, { allowJoin: true });
+ const team = await createTeam(hunt.id, creator.id, { password: 'abc123' });
+ await createUser({ email: 'joiner@test.dev' });
+
+ const client = newClient();
+ await client.login('joiner@test.dev', TEST_PASSWORD);
+ expect(
+ (
+ await client.post(`/api/hunt/${hunt.id}/join`, {
+ team: team.id,
+ password: 'wrong'
+ })
+ ).status
+ ).toBe(404);
+ expect(
+ (
+ await client.post(`/api/hunt/${hunt.id}/join`, {
+ team: team.id + 999,
+ password: 'abc123'
+ })
+ ).status
+ ).toBe(404);
+ });
+
+ it('rejects joining twice with 409', async () => {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id, { allowJoin: true });
+ const team = await createTeam(hunt.id, creator.id, { password: 'abc123' });
+ const player = await createUser({ email: 'joiner@test.dev' });
+ await createTeamMember(team.id, player.id);
+
+ const client = newClient();
+ await client.login('joiner@test.dev', TEST_PASSWORD);
+ const res = await client.post(`/api/hunt/${hunt.id}/join`, {
+ team: team.id,
+ password: 'abc123'
+ });
+ expect(res.status).toBe(409);
+ });
+});
diff --git a/tests/integration/pglite-host.mjs b/tests/integration/pglite-host.mjs
new file mode 100644
index 0000000..ee4d602
--- /dev/null
+++ b/tests/integration/pglite-host.mjs
@@ -0,0 +1,26 @@
+// Hosts an in-memory PGlite database behind a TCP socket speaking the
+// Postgres wire protocol. Spawned by global-setup.ts as a child process.
+//
+// Deliberately plain .mjs so it can run under a bare `node` with no
+// TS loader, and deliberately never calls db.close()/server.stop():
+// under Bun, PGlite teardown triggers an Emscripten exit(99) which would
+// kill the process with a non-zero code. Cleanup happens via SIGTERM/SIGKILL.
+import { PGlite } from '@electric-sql/pglite';
+import { PGLiteSocketServer } from '@electric-sql/pglite-socket';
+
+const db = new PGlite();
+await db.waitReady;
+
+const server = new PGLiteSocketServer({
+ db,
+ port: 0,
+ host: '127.0.0.1',
+ maxConnections: 20
+});
+await server.start();
+
+const [host, port] = server.getServerConn().split(':');
+console.log(`PGLITE_READY postgresql://postgres@${host}:${port}/postgres`);
+
+process.on('SIGTERM', () => process.exit(0));
+process.on('SIGINT', () => process.exit(0));
diff --git a/tests/integration/review.itest.ts b/tests/integration/review.itest.ts
new file mode 100644
index 0000000..e79561e
--- /dev/null
+++ b/tests/integration/review.itest.ts
@@ -0,0 +1,186 @@
+import superjson from 'superjson';
+import { beforeEach, describe, expect, it } from 'vitest';
+import { newClient } from '../helpers/api';
+import { resetDatabase } from '../helpers/database';
+import {
+ createAnswer,
+ createHunt,
+ createHuntMember,
+ createQuest,
+ createTeam,
+ createUser,
+ TEST_PASSWORD
+} from '../helpers/factories';
+import { prisma } from './setup';
+
+beforeEach(resetDatabase);
+
+/** Hunt with one quest, one team and a submitted (non-final) answer. */
+async function seedAnswer() {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id);
+ const quest = await createQuest(hunt.id);
+ const owner = await createUser();
+ const team = await createTeam(hunt.id, owner.id);
+ const answer = await createAnswer({
+ teamId: team.id,
+ questId: quest.id,
+ memberId: owner.id
+ });
+ return { creator, hunt, quest, owner, team, answer };
+}
+
+const reviewBody = {
+ score: 42,
+ review: 'Nice shot!',
+ showcase: true,
+ internalNote: 'check exposure',
+ rankingValue: 7
+};
+
+describe('review answer (POST /api/admin/answer/[answerId]/review)', () => {
+ it('requires a session', async () => {
+ const res = await newClient().post('/api/admin/answer/1/review', {
+ ...reviewBody,
+ showcase: false
+ });
+ expect(res.status).toBe(401);
+ });
+
+ it('returns 404 for unknown answers', async () => {
+ const { hunt } = await seedAnswer();
+ const organizer = await createUser({ email: 'organizer@test.dev' });
+ await createHuntMember(hunt.id, organizer.id);
+
+ const client = newClient();
+ await client.login('organizer@test.dev', TEST_PASSWORD);
+ const res = await client.post('/api/admin/answer/999999/review', {
+ ...reviewBody,
+ showcase: false
+ });
+ expect(res.status).toBe(404);
+ });
+
+ it('denies non-members of the hunt with 404', async () => {
+ const { answer } = await seedAnswer();
+ await createUser({ email: 'stranger@test.dev' });
+
+ const client = newClient();
+ await client.login('stranger@test.dev', TEST_PASSWORD);
+ const res = await client.post(
+ `/api/admin/answer/${answer.id}/review`,
+ reviewBody
+ );
+ expect(res.status).toBe(404);
+ expect(await prisma.review.count()).toBe(0);
+ });
+
+ it('creates a review as a hunt member', async () => {
+ const { hunt, answer } = await seedAnswer();
+ const organizer = await createUser({ email: 'organizer@test.dev' });
+ await createHuntMember(hunt.id, organizer.id);
+
+ const client = newClient();
+ await client.login('organizer@test.dev', TEST_PASSWORD);
+ const res = await client.post(
+ `/api/admin/answer/${answer.id}/review`,
+ reviewBody
+ );
+ expect(res.status).toBe(200);
+ expect(await res.json()).toEqual({ id: answer.id });
+
+ const review = await prisma.review.findUniqueOrThrow({
+ where: { answerId: answer.id }
+ });
+ expect(review).toMatchObject({
+ score: 42,
+ review: 'Nice shot!',
+ showcase: true,
+ internalNote: 'check exposure',
+ rankingValue: 7,
+ reviewerId: organizer.id
+ });
+ });
+
+ it('updates the existing review on a second pass', async () => {
+ const { hunt, answer } = await seedAnswer();
+ const first = await createUser({ email: 'first@test.dev' });
+ const second = await createUser({ email: 'second@test.dev' });
+ await createHuntMember(hunt.id, first.id);
+ await createHuntMember(hunt.id, second.id);
+
+ const firstClient = newClient();
+ await firstClient.login('first@test.dev', TEST_PASSWORD);
+ await firstClient.post(`/api/admin/answer/${answer.id}/review`, reviewBody);
+
+ const secondClient = newClient();
+ await secondClient.login('second@test.dev', TEST_PASSWORD);
+ const res = await secondClient.post(
+ `/api/admin/answer/${answer.id}/review`,
+ { ...reviewBody, score: 100, showcase: false }
+ );
+ expect(res.status).toBe(200);
+
+ const reviews = await prisma.review.findMany({
+ where: { answerId: answer.id }
+ });
+ expect(reviews).toHaveLength(1);
+ expect(reviews[0]).toMatchObject({
+ score: 100,
+ showcase: false,
+ reviewerId: second.id
+ });
+ });
+
+ it('rejects invalid scores with 400', async () => {
+ const { hunt, answer } = await seedAnswer();
+ const organizer = await createUser({ email: 'organizer@test.dev' });
+ await createHuntMember(hunt.id, organizer.id);
+
+ const client = newClient();
+ await client.login('organizer@test.dev', TEST_PASSWORD);
+ const res = await client.post(`/api/admin/answer/${answer.id}/review`, {
+ ...reviewBody,
+ score: -5
+ });
+ expect(res.status).toBe(400);
+ });
+});
+
+describe('hunt admin detail (GET /api/admin/hunt/[huntId])', () => {
+ it('denies non-members with 404', async () => {
+ const { hunt } = await seedAnswer();
+ await createUser({ email: 'stranger@test.dev' });
+
+ const client = newClient();
+ await client.login('stranger@test.dev', TEST_PASSWORD);
+ const res = await client.get(`/api/admin/hunt/${hunt.id}`);
+ expect(res.status).toBe(404);
+ });
+
+ it('responds with a SuperJSON envelope containing real Dates', async () => {
+ const { hunt } = await seedAnswer();
+ const organizer = await createUser({ email: 'organizer@test.dev' });
+ await createHuntMember(hunt.id, organizer.id);
+ const start = new Date('2026-09-01T12:00:00Z');
+ await prisma.hunt.update({
+ where: { id: hunt.id },
+ data: { start }
+ });
+
+ const client = newClient();
+ await client.login('organizer@test.dev', TEST_PASSWORD);
+ const res = await client.get(`/api/admin/hunt/${hunt.id}`);
+ expect(res.status).toBe(200);
+ const raw = await res.text();
+ const envelope = JSON.parse(raw);
+ // SuperJSON wraps the payload in { json, meta }
+ expect(envelope.json).toBeDefined();
+ expect(envelope.meta).toBeDefined();
+
+ const huntDto = superjson.parse<{ start: Date; id: number }>(raw);
+ expect(huntDto.id).toBe(hunt.id);
+ expect(huntDto.start).toBeInstanceOf(Date);
+ expect(huntDto.start.toISOString()).toBe(start.toISOString());
+ });
+});
diff --git a/tests/integration/setup.ts b/tests/integration/setup.ts
new file mode 100644
index 0000000..b3e9ab7
--- /dev/null
+++ b/tests/integration/setup.ts
@@ -0,0 +1,11 @@
+import { inject } from 'vitest';
+
+export const baseUrl = inject('baseUrl');
+
+// Must be set before the first import of ~~/lib/prisma, which reads
+// DATABASE_URL at import time.
+process.env.DATABASE_URL = inject('databaseUrl');
+
+const { default: prisma } = await import('~~/lib/prisma');
+
+export { prisma };
diff --git a/tests/integration/showcase.itest.ts b/tests/integration/showcase.itest.ts
new file mode 100644
index 0000000..75e50fe
--- /dev/null
+++ b/tests/integration/showcase.itest.ts
@@ -0,0 +1,145 @@
+import { beforeEach, describe, expect, it } from 'vitest';
+import { newClient } from '../helpers/api';
+import { resetDatabase } from '../helpers/database';
+import {
+ createAnswer,
+ createFile,
+ createHunt,
+ createHuntMember,
+ createQuest,
+ createReview,
+ createTeam,
+ createTeamMember,
+ createUser,
+ TEST_PASSWORD
+} from '../helpers/factories';
+import { prisma } from './setup';
+
+beforeEach(resetDatabase);
+
+/**
+ * Hunt with two teams and three showcase-worthy answers:
+ * - a public picture with score 20 (team A)
+ * - a private picture with score 50 (team A, must be hidden everywhere)
+ * - a public picture with score 30 (team B)
+ */
+async function seedShowcase(reveal: boolean) {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id, {
+ revealAnswers: reveal,
+ revealQuests: reveal
+ });
+
+ const quest = await createQuest(hunt.id);
+
+ const ownerA = await createUser();
+ const teamA = await createTeam(hunt.id, ownerA.id, { name: 'Team A' });
+ const memberA = await createUser();
+ await createTeamMember(teamA.id, memberA.id);
+
+ const ownerB = await createUser();
+ const teamB = await createTeam(hunt.id, ownerB.id, { name: 'Team B' });
+
+ const answerA = await createAnswer({
+ teamId: teamA.id,
+ questId: quest.id,
+ memberId: ownerA.id
+ });
+ const publicA = await createFile(ownerA.id, { answerId: answerA.id });
+ await prisma.questAnswer.update({
+ where: { id: answerA.id },
+ data: { pictureId: publicA.id }
+ });
+ await createReview(answerA.id, creator.id, { score: 20, showcase: true });
+
+ const answerPrivate = await createAnswer({
+ teamId: teamA.id,
+ questId: quest.id,
+ memberId: ownerA.id
+ });
+ const privateA = await createFile(ownerA.id, {
+ answerId: answerPrivate.id,
+ private: true
+ });
+ await prisma.questAnswer.update({
+ where: { id: answerPrivate.id },
+ data: { pictureId: privateA.id }
+ });
+ await createReview(answerPrivate.id, creator.id, {
+ score: 50,
+ showcase: true
+ });
+
+ const answerB = await createAnswer({
+ teamId: teamB.id,
+ questId: quest.id,
+ memberId: ownerB.id
+ });
+ const publicB = await createFile(ownerB.id, { answerId: answerB.id });
+ await prisma.questAnswer.update({
+ where: { id: answerB.id },
+ data: { pictureId: publicB.id }
+ });
+ await createReview(answerB.id, creator.id, { score: 30, showcase: true });
+
+ return { creator, hunt, quest, teamA, teamB, answerA, answerB };
+}
+
+describe('showcase (GET /api/hunt/[huntId]/showcase)', () => {
+ it('returns 404 for unknown hunts', async () => {
+ const res = await newClient().get('/api/hunt/999999/showcase');
+ expect(res.status).toBe(404);
+ });
+
+ it('blocks anonymous users before answers are revealed', async () => {
+ const { hunt } = await seedShowcase(false);
+ const res = await newClient().get(`/api/hunt/${hunt.id}/showcase`);
+ expect(res.status).toBe(403);
+ });
+
+ it('lets hunt members peek before reveal', async () => {
+ const { hunt } = await seedShowcase(false);
+ const organizer = await createUser({ email: 'organizer@test.dev' });
+ await createHuntMember(hunt.id, organizer.id);
+
+ const client = newClient();
+ await client.login('organizer@test.dev', TEST_PASSWORD);
+ const res = await client.get(`/api/hunt/${hunt.id}/showcase`);
+ expect(res.status).toBe(200);
+ });
+
+ it('builds leaderboard, counts and showcase answers once revealed', async () => {
+ const { hunt, quest, teamA, teamB, answerA, answerB } =
+ await seedShowcase(true);
+
+ const res = await newClient().get(`/api/hunt/${hunt.id}/showcase`);
+ expect(res.status).toBe(200);
+ const body = await res.json();
+
+ // 2 owners + 1 extra member
+ expect(body.userCount).toBe(3);
+ // all three answer pictures are counted, including the private one
+ expect(body.pictureCount).toBe(3);
+ // the private picture's score leaks into the leaderboard totals
+ expect(body.totalScore).toBe(20 + 50 + 30);
+ expect(body.leaderboard).toEqual([
+ expect.objectContaining({ name: 'Team A', score: 70, answers: 2 }),
+ expect.objectContaining({ name: 'Team B', score: 30, answers: 1 })
+ ]);
+ expect(body.leaderboard[0].id).toBe(teamA.id);
+ expect(body.leaderboard[1].id).toBe(teamB.id);
+
+ // only the quest with showcase answers is included
+ expect(body.quests).toHaveLength(1);
+ expect(body.quests[0].id).toBe(quest.id);
+ // only public showcase answers cross teams, sorted by score ascending
+ expect(body.quests[0].answers).toHaveLength(2);
+ expect(body.quests[0].answers.map((a: { id: number }) => a.id)).toEqual([
+ answerA.id,
+ answerB.id
+ ]);
+ for (const answer of body.quests[0].answers) {
+ expect([teamA.id, teamB.id]).toContain(answer.team.id);
+ }
+ });
+});
diff --git a/tests/integration/submit.itest.ts b/tests/integration/submit.itest.ts
new file mode 100644
index 0000000..99536ed
--- /dev/null
+++ b/tests/integration/submit.itest.ts
@@ -0,0 +1,195 @@
+import { beforeEach, describe, expect, it } from 'vitest';
+import { newClient } from '../helpers/api';
+import { resetDatabase } from '../helpers/database';
+import {
+ createAnswer,
+ createHunt,
+ createQuest,
+ createTeam,
+ createUser,
+ TEST_PASSWORD
+} from '../helpers/factories';
+import { prisma } from './setup';
+
+beforeEach(resetDatabase);
+
+/** Hunt (with revealed quests), a quest and a team owned by the given user. */
+async function seedPlayground(ownerEmail: string) {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id, { revealQuests: true });
+ const quest = await createQuest(hunt.id, { textRequired: true });
+ const owner = await createUser({ email: ownerEmail });
+ const team = await createTeam(hunt.id, owner.id);
+ return { hunt, quest, owner, team };
+}
+
+function form(fields: Record) {
+ const data = new FormData();
+ for (const [key, value] of Object.entries(fields)) {
+ data.append(key, value);
+ }
+ return data;
+}
+
+describe('submit answer (POST /api/quest/[questId]/submit)', () => {
+ it('requires a session', async () => {
+ const res = await newClient().post(
+ '/api/quest/1/submit',
+ form({ answer: 'hello' })
+ );
+ expect(res.status).toBe(401);
+ });
+
+ it('returns 404 for unknown quests', async () => {
+ await createUser({ email: 'owner@test.dev' });
+ const client = newClient();
+ await client.login('owner@test.dev', TEST_PASSWORD);
+ const res = await client.post(
+ '/api/quest/999999/submit',
+ form({ answer: 'hello' })
+ );
+ expect(res.status).toBe(404);
+ });
+
+ it('returns 403 when the user has no team in the hunt', async () => {
+ const { quest } = await seedPlayground('owner@test.dev');
+ const loner = await createUser({ email: 'loner@test.dev' });
+
+ const client = newClient();
+ await client.login('loner@test.dev', TEST_PASSWORD);
+ const res = await client.post(
+ `/api/quest/${quest.id}/submit`,
+ form({ answer: 'hello' })
+ );
+ expect(res.status).toBe(403);
+ expect(
+ await prisma.questAnswer.count({ where: { memberId: loner.id } })
+ ).toBe(0);
+ });
+
+ it('stores a text-only answer', async () => {
+ const { quest, team, owner } = await seedPlayground('owner@test.dev');
+
+ const client = newClient();
+ await client.login('owner@test.dev', TEST_PASSWORD);
+ const res = await client.post(
+ `/api/quest/${quest.id}/submit`,
+ form({ answer: 'the answer', lang: 'en' })
+ );
+ expect(res.status).toBe(200);
+ expect(await res.json()).toEqual({ ok: true });
+
+ const answer = await prisma.questAnswer.findFirst({
+ where: { teamId: team.id, questId: quest.id }
+ });
+ expect(answer).toMatchObject({
+ text: 'the answer',
+ lang: 'en',
+ final: false,
+ memberId: owner.id
+ });
+ });
+
+ it('exposes the stored answer with ISO dates via the quest endpoint', async () => {
+ const { quest } = await seedPlayground('owner@test.dev');
+
+ const client = newClient();
+ await client.login('owner@test.dev', TEST_PASSWORD);
+ await client.post(
+ `/api/quest/${quest.id}/submit`,
+ form({ answer: 'the answer' })
+ );
+
+ const res = await client.get(`/api/quest/${quest.id}`);
+ expect(res.status).toBe(200);
+ const body = await res.json();
+ expect(body.answer.text).toBe('the answer');
+ expect(new Date(body.answer.createdAt).getTime()).not.toBeNaN();
+ expect(new Date(body.answer.updatedAt).getTime()).not.toBeNaN();
+ });
+
+ it('updates an existing answer when updatedAt is current', async () => {
+ const { quest, team } = await seedPlayground('owner@test.dev');
+
+ const client = newClient();
+ await client.login('owner@test.dev', TEST_PASSWORD);
+ await client.post(
+ `/api/quest/${quest.id}/submit`,
+ form({ answer: 'first try' })
+ );
+
+ const existing = await prisma.questAnswer.findFirstOrThrow({
+ where: { teamId: team.id, questId: quest.id }
+ });
+
+ const res = await client.post(
+ `/api/quest/${quest.id}/submit`,
+ form({
+ answer: 'final try',
+ updatedAt: existing.updatedAt.toISOString(),
+ final: 'true'
+ })
+ );
+ expect(res.status).toBe(200);
+
+ const updated = await prisma.questAnswer.findFirstOrThrow({
+ where: { teamId: team.id, questId: quest.id }
+ });
+ expect(updated.id).toBe(existing.id);
+ expect(updated.text).toBe('final try');
+ expect(updated.final).toBe(true);
+ });
+
+ it('rejects updates based on stale data with 409', async () => {
+ const { quest } = await seedPlayground('owner@test.dev');
+
+ const client = newClient();
+ await client.login('owner@test.dev', TEST_PASSWORD);
+ await client.post(
+ `/api/quest/${quest.id}/submit`,
+ form({ answer: 'first try' })
+ );
+
+ // missing updatedAt although an answer exists
+ expect(
+ (
+ await client.post(
+ `/api/quest/${quest.id}/submit`,
+ form({ answer: 'second try' })
+ )
+ ).status
+ ).toBe(409);
+
+ // outdated updatedAt
+ expect(
+ (
+ await client.post(
+ `/api/quest/${quest.id}/submit`,
+ form({
+ answer: 'second try',
+ updatedAt: new Date(Date.now() - 60_000).toISOString()
+ })
+ )
+ ).status
+ ).toBe(409);
+ });
+
+ it('rejects submissions for finalized answers with 418', async () => {
+ const { quest, team, owner } = await seedPlayground('owner@test.dev');
+ await createAnswer(
+ { teamId: team.id, questId: quest.id, memberId: owner.id },
+ { text: 'locked in', final: true }
+ );
+
+ const client = newClient();
+ await client.login('owner@test.dev', TEST_PASSWORD);
+ const res = await client.post(
+ `/api/quest/${quest.id}/submit`,
+ form({
+ answer: 'too late',
+ updatedAt: new Date().toISOString()
+ })
+ );
+ expect(res.status).toBe(418);
+ });
+});
diff --git a/tests/integration/team.itest.ts b/tests/integration/team.itest.ts
new file mode 100644
index 0000000..908c84b
--- /dev/null
+++ b/tests/integration/team.itest.ts
@@ -0,0 +1,141 @@
+import { beforeEach, describe, expect, it } from 'vitest';
+import { newClient } from '../helpers/api';
+import { resetDatabase } from '../helpers/database';
+import {
+ createHunt,
+ createTeam,
+ createTeamMember,
+ createUser,
+ TEST_PASSWORD
+} from '../helpers/factories';
+import { prisma } from './setup';
+
+beforeEach(resetDatabase);
+
+describe('create team (POST /api/hunt/[huntId]/team)', () => {
+ it('requires a session', async () => {
+ const res = await newClient().post('/api/hunt/1/team', {
+ name: 'The A-Team'
+ });
+ expect(res.status).toBe(401);
+ });
+
+ it('creates a team with a generated invite password', async () => {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id);
+ const owner = await createUser({ email: 'owner@test.dev' });
+
+ const client = newClient();
+ await client.login('owner@test.dev', TEST_PASSWORD);
+ const res = await client.post(`/api/hunt/${hunt.id}/team`, {
+ name: 'The A-Team',
+ description: 'We love photos'
+ });
+ expect(res.status).toBe(200);
+ const body = await res.json();
+ expect(body.team).toMatchObject({
+ name: 'The A-Team',
+ description: 'We love photos',
+ ownerId: owner.id,
+ huntId: hunt.id
+ });
+ // short hex invite code
+ expect(body.team.password).toMatch(/^[0-9a-f]{6}$/);
+
+ expect(await prisma.huntTeam.count({ where: { huntId: hunt.id } })).toBe(1);
+ });
+
+ it('enforces one team per user per hunt', async () => {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id);
+ await createUser({ email: 'owner@test.dev' });
+
+ const client = newClient();
+ await client.login('owner@test.dev', TEST_PASSWORD);
+ expect(
+ (await client.post(`/api/hunt/${hunt.id}/team`, { name: 'First' })).status
+ ).toBe(200);
+ // as owner of the first team
+ expect(
+ (await client.post(`/api/hunt/${hunt.id}/team`, { name: 'Second' }))
+ .status
+ ).toBe(409);
+
+ // as member of an existing team
+ const team = await createTeam(hunt.id, creator.id);
+ const member = await createUser({ email: 'member@test.dev' });
+ await createTeamMember(team.id, member.id);
+ const memberClient = newClient();
+ await memberClient.login('member@test.dev', TEST_PASSWORD);
+ expect(
+ (await memberClient.post(`/api/hunt/${hunt.id}/team`, { name: 'Third' }))
+ .status
+ ).toBe(409);
+ });
+
+ it('rejects invalid team names with 400', async () => {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id);
+
+ const client = newClient();
+ await client.login(creator.email, TEST_PASSWORD);
+ expect(
+ (await client.post(`/api/hunt/${hunt.id}/team`, { name: 'ab' })).status
+ ).toBe(400);
+ });
+});
+
+describe('teams overview (GET /api/hunt/[huntId]/teams)', () => {
+ it('returns teams with owner name and member count, sorted by name', async () => {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id);
+ const ownerA = await createUser({ name: 'Alice' });
+ const ownerB = await createUser({ name: 'Bob' });
+ const teamA = await createTeam(hunt.id, ownerA.id, { name: 'Alpha' });
+ await createTeam(hunt.id, ownerB.id, { name: 'Zebra' });
+ const joiner = await createUser();
+ await createTeamMember(teamA.id, joiner.id);
+
+ const res = await newClient().get(`/api/hunt/${hunt.id}/teams`);
+ expect(res.status).toBe(200);
+ const teams = await res.json();
+ expect(teams).toEqual([
+ {
+ id: teamA.id,
+ name: 'Alpha',
+ description: '',
+ owner: { name: 'Alice' },
+ _count: { members: 1 }
+ },
+ expect.objectContaining({
+ name: 'Zebra',
+ owner: { name: 'Bob' },
+ _count: { members: 0 }
+ })
+ ]);
+ // the invite password must not leak here
+ expect(JSON.stringify(teams)).not.toContain('password');
+ });
+});
+
+describe('join via team password (POST /api/hunt/[huntId]/join)', () => {
+ it('lets a second user join an existing team', async () => {
+ const creator = await createUser();
+ const hunt = await createHunt(creator.id);
+ const owner = await createUser();
+ const team = await createTeam(hunt.id, owner.id, { password: 'feedbe' });
+ await createUser({ email: 'joiner@test.dev' });
+
+ const client = newClient();
+ await client.login('joiner@test.dev', TEST_PASSWORD);
+ const res = await client.post(`/api/hunt/${hunt.id}/join`, {
+ team: team.id,
+ password: 'feedbe'
+ });
+ expect(res.status).toBe(200);
+
+ const teamsRes = await client.get(`/api/hunt/${hunt.id}/teams`);
+ const teams = await teamsRes.json();
+ expect(teams[0]._count.members).toBe(1);
+ });
+});
diff --git a/tests/integration/types.d.ts b/tests/integration/types.d.ts
new file mode 100644
index 0000000..d46507a
--- /dev/null
+++ b/tests/integration/types.d.ts
@@ -0,0 +1,9 @@
+export {};
+
+declare module 'vitest' {
+ // Values provided by global-setup.ts, consumed via inject() in setup.ts.
+ export interface ProvidedContext {
+ baseUrl: string;
+ databaseUrl: string;
+ }
+}
diff --git a/tests/tsconfig.json b/tests/tsconfig.json
new file mode 100644
index 0000000..94d7dc4
--- /dev/null
+++ b/tests/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ // Integration tests + helpers. Picked up by `nuxt typecheck` through the
+ // reference in the root tsconfig.json. Mirrors the options of the
+ // Nuxt-generated tsconfigs, minus noUncheckedIndexedAccess (tests assert
+ // on indexed access all the time).
+ "compilerOptions": {
+ "paths": {
+ "~~": [".."],
+ "~~/*": ["../*"],
+ "#shared": ["../shared"],
+ "#shared/*": ["../shared/*"]
+ },
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "target": "ESNext",
+ "resolveJsonModule": true,
+ "moduleDetection": "force",
+ "isolatedModules": true,
+ "verbatimModuleSyntax": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noImplicitOverride": true,
+ "module": "preserve",
+ "noEmit": true,
+ "types": ["node"],
+ "moduleResolution": "Bundler",
+ "useDefineForClassFields": true,
+ "noImplicitThis": true,
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["./**/*.ts", "../vitest.integration.config.ts"],
+ "exclude": ["../node_modules"]
+}
diff --git a/tsconfig.json b/tsconfig.json
index 307b213..727de24 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -13,6 +13,9 @@
},
{
"path": "./.nuxt/tsconfig.node.json"
+ },
+ {
+ "path": "./tests/tsconfig.json"
}
]
}
diff --git a/vitest.integration.config.ts b/vitest.integration.config.ts
new file mode 100644
index 0000000..1f744f9
--- /dev/null
+++ b/vitest.integration.config.ts
@@ -0,0 +1,23 @@
+import { fileURLToPath } from 'node:url';
+import { defineConfig } from 'vitest/config';
+
+const root = (path: string) => fileURLToPath(new URL(path, import.meta.url));
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ '~~': root('.'),
+ '#shared': root('./shared')
+ }
+ },
+ test: {
+ include: ['tests/integration/**/*.itest.ts'],
+ globalSetup: ['tests/integration/global-setup.ts'],
+ setupFiles: ['tests/integration/setup.ts'],
+ fileParallelism: false,
+ // First touch of WASM Postgres can be slow; the build happens in
+ // globalSetup and is not affected by these timeouts.
+ testTimeout: 30_000,
+ hookTimeout: 120_000
+ }
+});