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 + } +});