From 73b71612515936f2431f17ea653a1b5debcffe45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 8 May 2026 13:50:07 +0200 Subject: [PATCH 1/3] test(integration): Postgres testcontainers + 4 critical-path tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an integration test suite that runs against a real Postgres booted in-process via testcontainers. Lives under tests/integration/, runs via `pnpm test:integration`, and is excluded from the default `pnpm test` loop so the 477-test unit suite stays fast (~1.2s). Covers four regression-prone behaviours that pure unit tests cannot validate against a mocked client: - rate-limit.test.ts — atomic UPSERT under concurrency (5/6 allowed) and window-reset semantics for src/lib/rate-limit.ts - idempotency-replay.test.ts — cached replay with X-Idempotent-Replay, expired-row purge, and the do-not-cache contract for 401/403/408/429/5xx on top of src/lib/idempotency.ts - cascade-delete.test.ts — GDPR Art. 17 erasure: user.delete wipes every onDelete:Cascade table and SetNulls AuditLog/Feedback - auth-flow.test.ts — createSession/getSession round-trip plus expired-session purge for src/lib/auth/session.ts Adds .github/workflows/integration.yml triggering on pull_request and push to main. Local Docker is not running in this worktree environment; the suite was verified by collecting all 10 tests via `vitest list` and will execute in CI. Co-Authored-By: Marc-André Bombeck Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/integration.yml | 34 + package.json | 5 +- pnpm-lock.yaml | 980 ++++++++++++++++++- tests/integration/auth-flow.test.ts | 119 +++ tests/integration/cascade-delete.test.ts | 203 ++++ tests/integration/idempotency-replay.test.ts | 180 ++++ tests/integration/rate-limit.test.ts | 88 ++ tests/integration/setup.ts | 156 +++ vitest.integration.config.mts | 33 + 9 files changed, 1786 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/integration.yml create mode 100644 tests/integration/auth-flow.test.ts create mode 100644 tests/integration/cascade-delete.test.ts create mode 100644 tests/integration/idempotency-replay.test.ts create mode 100644 tests/integration/rate-limit.test.ts create mode 100644 tests/integration/setup.ts create mode 100644 vitest.integration.config.mts diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000..2ec5205 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,34 @@ +name: Integration tests + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + integration: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - run: pnpm db:generate + + - run: pnpm test:integration + env: + # The integration suite boots a Postgres testcontainer and points + # Prisma at it via DATABASE_URL set inside `tests/integration/setup.ts`. + # These env vars satisfy code paths that read them at module load. + API_TOKEN_HMAC_KEY: integration-tests-do-not-use-in-prod + ENCRYPTION_KEY: 0000000000000000000000000000000000000000000000000000000000000000 + SESSION_SECRET: integration-tests diff --git a/package.json b/package.json index e228ff8..d6d46a1 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "format:check": "prettier --check .", "typecheck": "tsc --noEmit", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "test:integration": "vitest run --config vitest.integration.config.mts" }, "dependencies": { "@aws-sdk/client-s3": "^3.1045.0", @@ -51,6 +52,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@testcontainers/postgresql": "^11.14.0", "@types/node": "^25", "@types/react": "^19", "@types/react-dom": "^19", @@ -64,6 +66,7 @@ "prettier-plugin-tailwindcss": "^0.8.0", "shadcn": "^4.6.0", "tailwindcss": "^4", + "testcontainers": "^11.14.0", "tw-animate-css": "^1.4.0", "typescript": "^6", "vitest": "^4.1.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1857c9a..3c8d6cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,9 @@ importers: '@tailwindcss/postcss': specifier: ^4 version: 4.2.4 + '@testcontainers/postgresql': + specifier: ^11.14.0 + version: 11.14.0 '@types/node': specifier: ^25 version: 25.6.0 @@ -110,7 +113,7 @@ importers: version: 3.6.4 '@vitejs/plugin-react': specifier: ^5.1.4 - version: 5.1.4(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)) + version: 5.1.4(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.4)) '@vitest/coverage-v8': specifier: ^4.1.5 version: 4.1.5(vitest@4.1.5) @@ -135,6 +138,9 @@ importers: tailwindcss: specifier: ^4 version: 4.2.4 + testcontainers: + specifier: ^11.14.0 + version: 11.14.0 tw-animate-css: specifier: ^1.4.0 version: 1.4.0 @@ -143,7 +149,7 @@ importers: version: 6.0.3 vitest: specifier: ^4.1.5 - version: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(msw@2.14.2(@types/node@25.6.0)(typescript@6.0.3))(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)) + version: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(msw@2.14.2(@types/node@25.6.0)(typescript@6.0.3))(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.4)) packages: @@ -455,6 +461,9 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@balena/dockerignore@1.0.2': + resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -704,6 +713,20 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@grpc/grpc-js@1.14.3': + resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@grpc/proto-loader@0.8.1': + resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} + engines: {node: '>=6'} + hasBin: true + '@hexagon/base64@1.1.28': resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} @@ -928,6 +951,10 @@ packages: '@types/node': optional: true + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -944,9 +971,15 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@kwsites/file-exists@1.1.1': + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + '@levischuck/tiny-cbor@0.2.11': resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} @@ -1265,6 +1298,10 @@ packages: resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==} engines: {node: '>=20.0.0'} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@prisma/adapter-pg@7.8.0': resolution: {integrity: sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg==} @@ -1328,6 +1365,36 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.1': + resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -2508,6 +2575,9 @@ packages: peerDependencies: react: ^18 || ^19 + '@testcontainers/postgresql@11.14.0': + resolution: {integrity: sha512-wYbJn8GRTj8qfqzfVubxioYWlHJU/ImIjuzPwyy9C5Qfo6g3GLduPZAj+BifvqTZjgT3gd4gFVLCPhBji7dc1w==} + '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} @@ -2559,6 +2629,12 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/docker-modem@3.0.6': + resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} + + '@types/dockerode@4.0.1': + resolution: {integrity: sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -2568,6 +2644,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@25.6.0': resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} @@ -2591,6 +2670,15 @@ packages: '@types/set-cookie-parser@2.4.10': resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} + '@types/ssh2-streams@0.1.13': + resolution: {integrity: sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==} + + '@types/ssh2@0.5.52': + resolution: {integrity: sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==} + + '@types/ssh2@1.15.5': + resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==} + '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} @@ -2812,6 +2900,10 @@ packages: '@vitest/utils@4.1.5': resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -2856,6 +2948,18 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2902,6 +3006,9 @@ packages: asn1.js@5.4.1: resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + asn1js@3.0.7: resolution: {integrity: sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==} engines: {node: '>=12.0.0'} @@ -2924,6 +3031,12 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async-lock@1.4.1: + resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -2940,6 +3053,14 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2947,10 +3068,54 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.7.1: + resolution: {integrity: sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.9.1: + resolution: {integrity: sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.13.1: + resolution: {integrity: sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.4.3: + resolution: {integrity: sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==} + base64-arraybuffer@1.0.2: resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} engines: {node: '>= 0.6.0'} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.20: resolution: {integrity: sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==} engines: {node: '>=6.0.0'} @@ -2961,9 +3126,15 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + better-result@2.8.2: resolution: {integrity: sha512-YOf0VSj5nUPI27doTtXF+BBnsiRq3qY7avHqfIWnppxTLGyvkLq1QV2RTxkwoZwJ60ywLfZ0raFF4J/G886i7A==} + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bn.js@4.12.3: resolution: {integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==} @@ -2980,6 +3151,9 @@ packages: brace-expansion@1.1.14: resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} @@ -2993,13 +3167,31 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + buildcheck@0.0.7: + resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} + engines: {node: '>=10.0.0'} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + byline@5.0.0: + resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} + engines: {node: '>=0.10.0'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -3058,6 +3250,9 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -3102,6 +3297,10 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -3134,6 +3333,9 @@ packages: core-js@3.49.0: resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} @@ -3147,6 +3349,19 @@ packages: typescript: optional: true + cpu-features@0.0.10: + resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} + engines: {node: '>=10.0.0'} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + cron-parser@5.5.0: resolution: {integrity: sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww==} engines: {node: '>=18'} @@ -3313,6 +3528,18 @@ packages: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} + docker-compose@1.4.2: + resolution: {integrity: sha512-rPHigTKGaEHpkUmfd69QgaOp+Os5vGJwG/Ry8lcr8W/382AmI+z/D7qoa9BybKIkqNppaIbs8RYeHSevdQjWww==} + engines: {node: '>= 6.0.0'} + + docker-modem@5.0.7: + resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==} + engines: {node: '>= 8.0'} + + dockerode@4.0.12: + resolution: {integrity: sha512-/bCZd6KlGcjZO8Buqmi/vXuqEGVEZ0PNjx/biBNqJD3MhK9DmdiAuKxqfNhflgDESDIiBz3qF+0e55+CpnrUcw==} + engines: {node: '>= 8.0'} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -3328,6 +3555,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -3361,6 +3591,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.21.0: resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==} engines: {node: '>=10.13.0'} @@ -3562,9 +3795,20 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + eventsource-parser@3.0.8: resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} engines: {node: '>=18.0.0'} @@ -3605,6 +3849,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} @@ -3707,6 +3954,9 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@11.3.4: resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} engines: {node: '>=14.14'} @@ -3763,6 +4013,10 @@ packages: get-port-please@3.2.0: resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + get-port@7.2.0: + resolution: {integrity: sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==} + engines: {node: '>=16'} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -3794,6 +4048,11 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -3901,6 +4160,9 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -4114,6 +4376,9 @@ packages: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -4140,6 +4405,9 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -4228,6 +4496,10 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -4313,9 +4585,15 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + log-symbols@6.0.0: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} @@ -4327,6 +4605,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -4405,9 +4686,29 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -4433,6 +4734,9 @@ packages: resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} engines: {node: '>=8.0.0'} + nan@2.26.2: + resolution: {integrity: sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -4502,6 +4806,10 @@ packages: resolution: {integrity: sha512-TMB1uHiGsHRGv1uYclfhivcnf0/PdFp2pNqRxXjncaAsjYMoisaQJI+SSZCqRq+VliwRTC8tsMQfmrWjDMhkPQ==} engines: {node: '>=20'} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -4594,6 +4902,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pako@2.1.0: resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} @@ -4635,6 +4946,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -4845,6 +5160,13 @@ packages: typescript: optional: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -4855,10 +5177,21 @@ packages: proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + properties-reader@3.0.1: + resolution: {integrity: sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==} + engines: {node: '>=18'} + + protobufjs@7.5.6: + resolution: {integrity: sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -4971,6 +5304,20 @@ packages: resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@5.0.0: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} @@ -5074,6 +5421,9 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -5192,6 +5542,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + split-ca@1.0.1: + resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -5200,6 +5553,13 @@ packages: resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} engines: {node: '>= 0.6'} + ssh-remote-port-forward@1.0.4: + resolution: {integrity: sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==} + + ssh2@1.17.0: + resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} + engines: {node: '>=10.16.0'} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -5228,6 +5588,9 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + streamx@2.25.0: + resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==} + strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} @@ -5235,6 +5598,10 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -5262,6 +5629,12 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-object@5.0.0: resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} engines: {node: '>=14.16'} @@ -5332,6 +5705,28 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-fs@3.1.2: + resolution: {integrity: sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar-stream@3.2.0: + resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + + testcontainers@11.14.0: + resolution: {integrity: sha512-r9pniwv/iwzyHaI7gwAvAm4Y+IvjJg3vBWdjrUCaDMc2AXIr4jKbq7jJO18Mw2ybs73pZy1Aj7p/4RVBGMRWjg==} + + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + text-segmentation@1.0.3: resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} @@ -5360,6 +5755,10 @@ packages: resolution: {integrity: sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==} hasBin: true + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -5401,6 +5800,9 @@ packages: tw-animate-css@1.4.0: resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -5445,9 +5847,16 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -5506,6 +5915,11 @@ packages: utrie@1.0.2: resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + valibot@1.2.0: resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} peerDependencies: @@ -5654,6 +6068,10 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -5672,6 +6090,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.4: + resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -5695,6 +6118,10 @@ packages: zeptomatch@2.1.0: resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + zod-to-json-schema@3.25.2: resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: @@ -6364,6 +6791,8 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@balena/dockerignore@1.0.2': {} + '@bcoe/v8-coverage@1.0.2': {} '@dotenvx/dotenvx@1.64.0': @@ -6555,6 +6984,25 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@grpc/grpc-js@1.14.3': + dependencies: + '@grpc/proto-loader': 0.8.1 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.6 + yargs: 17.7.2 + + '@grpc/proto-loader@0.8.1': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.6 + yargs: 17.7.2 + '@hexagon/base64@1.1.28': {} '@hono/node-server@1.19.11(hono@4.12.15)': @@ -6705,6 +7153,15 @@ snapshots: optionalDependencies: '@types/node': 25.6.0 + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6724,8 +7181,16 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-sdsl/ordered-map@4.4.2': {} + '@kurkle/color@0.3.4': {} + '@kwsites/file-exists@1.1.1': + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@levischuck/tiny-cbor@0.2.11': {} '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': @@ -7031,6 +7496,9 @@ snapshots: tslib: 2.8.1 tsyringe: 4.10.0 + '@pkgjs/parseargs@0.11.0': + optional: true + '@prisma/adapter-pg@7.8.0': dependencies: '@prisma/driver-adapter-utils': 7.8.0 @@ -7130,6 +7598,29 @@ snapshots: transitivePeerDependencies: - '@types/react-dom' + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.1 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.1': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.1': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -8402,6 +8893,15 @@ snapshots: '@tanstack/query-core': 5.100.9 react: 19.2.5 + '@testcontainers/postgresql@11.14.0': + dependencies: + testcontainers: 11.14.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + '@ts-morph/common@0.27.0': dependencies: fast-glob: 3.3.3 @@ -8465,12 +8965,27 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/docker-modem@3.0.6': + dependencies: + '@types/node': 25.6.0 + '@types/ssh2': 1.15.5 + + '@types/dockerode@4.0.1': + dependencies: + '@types/docker-modem': 3.0.6 + '@types/node': 25.6.0 + '@types/ssh2': 1.15.5 + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + '@types/node@25.6.0': dependencies: undici-types: 7.19.2 @@ -8498,6 +9013,19 @@ snapshots: dependencies: '@types/node': 25.6.0 + '@types/ssh2-streams@0.1.13': + dependencies: + '@types/node': 25.6.0 + + '@types/ssh2@0.5.52': + dependencies: + '@types/node': 25.6.0 + '@types/ssh2-streams': 0.1.13 + + '@types/ssh2@1.15.5': + dependencies: + '@types/node': 18.19.130 + '@types/statuses@2.0.6': {} '@types/trusted-types@2.0.7': @@ -8661,7 +9189,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0))': + '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.4))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -8669,7 +9197,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.4) transitivePeerDependencies: - supports-color @@ -8685,7 +9213,7 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(msw@2.14.2(@types/node@25.6.0)(typescript@6.0.3))(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)) + vitest: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(msw@2.14.2(@types/node@25.6.0)(typescript@6.0.3))(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.4)) '@vitest/expect@4.1.5': dependencies: @@ -8696,14 +9224,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.5(msw@2.14.2(@types/node@25.6.0)(typescript@6.0.3))(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0))': + '@vitest/mocker@4.1.5(msw@2.14.2(@types/node@25.6.0)(typescript@6.0.3))(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.4))': dependencies: '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.14.2(@types/node@25.6.0)(typescript@6.0.3) - vite: 7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.4) '@vitest/pretty-format@4.1.5': dependencies: @@ -8729,6 +9257,10 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -8768,6 +9300,32 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.3: {} + + archiver-utils@5.0.2: + dependencies: + glob: 10.5.0 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.18.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.2.0 + zip-stream: 6.0.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + argparse@2.0.1: {} aria-hidden@1.2.6: @@ -8850,6 +9408,10 @@ snapshots: minimalistic-assert: 1.0.1 safer-buffer: 2.1.2 + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + asn1js@3.0.7: dependencies: pvtsutils: 1.3.6 @@ -8872,6 +9434,10 @@ snapshots: async-function@1.0.0: {} + async-lock@1.4.1: {} + + async@3.2.6: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -8882,19 +9448,65 @@ snapshots: axobject-query@4.1.0: {} + b4a@1.8.1: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} + bare-events@2.8.2: {} + + bare-fs@4.7.1: + dependencies: + bare-events: 2.8.2 + bare-path: 3.0.0 + bare-stream: 2.13.1(bare-events@2.8.2) + bare-url: 2.4.3 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-os@3.9.1: {} + + bare-path@3.0.0: + dependencies: + bare-os: 3.9.1 + + bare-stream@2.13.1(bare-events@2.8.2): + dependencies: + streamx: 2.25.0 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.4.3: + dependencies: + bare-path: 3.0.0 + base64-arraybuffer@1.0.2: optional: true + base64-js@1.5.1: {} + baseline-browser-mapping@2.10.20: {} baseline-browser-mapping@2.10.27: {} + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + better-result@2.8.2: {} + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + bn.js@4.12.3: {} body-parser@2.2.2: @@ -8923,6 +9535,10 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -8939,12 +9555,29 @@ snapshots: node-releases: 2.0.38 update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer-crc32@1.0.0: {} + buffer-equal-constant-time@1.0.1: {} + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buildcheck@0.0.7: + optional: true + bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 + byline@5.0.0: {} + bytes@3.1.2: {} c12@3.3.4(magicast@0.5.2): @@ -9016,6 +9649,8 @@ snapshots: dependencies: readdirp: 5.0.0 + chownr@1.1.4: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -9050,6 +9685,14 @@ snapshots: commander@14.0.3: {} + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + concat-map@0.0.1: {} confbox@0.2.4: {} @@ -9069,6 +9712,8 @@ snapshots: core-js@3.49.0: optional: true + core-util-is@1.0.3: {} + cors@2.8.6: dependencies: object-assign: 4.1.1 @@ -9083,6 +9728,19 @@ snapshots: optionalDependencies: typescript: 6.0.3 + cpu-features@0.0.10: + dependencies: + buildcheck: 0.0.7 + nan: 2.26.2 + optional: true + + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + cron-parser@5.5.0: dependencies: luxon: 3.7.2 @@ -9215,6 +9873,31 @@ snapshots: diff@8.0.4: {} + docker-compose@1.4.2: + dependencies: + yaml: 2.8.4 + + docker-modem@5.0.7: + dependencies: + debug: 4.4.3 + readable-stream: 3.6.2 + split-ca: 1.0.1 + ssh2: 1.17.0 + transitivePeerDependencies: + - supports-color + + dockerode@4.0.12: + dependencies: + '@balena/dockerignore': 1.0.2 + '@grpc/grpc-js': 1.14.3 + '@grpc/proto-loader': 0.7.15 + docker-modem: 5.0.7 + protobufjs: 7.5.6 + tar-fs: 2.1.4 + uuid: 10.0.0 + transitivePeerDependencies: + - supports-color + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -9232,6 +9915,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 @@ -9262,6 +9947,10 @@ snapshots: encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.21.0: dependencies: graceful-fs: 4.2.11 @@ -9628,8 +10317,18 @@ snapshots: etag@1.8.1: {} + event-target-shim@5.0.1: {} + eventemitter3@5.0.4: {} + events-universal@1.0.1: + dependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + + events@3.3.0: {} + eventsource-parser@3.0.8: {} eventsource@3.0.7: @@ -9711,6 +10410,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-fifo@1.3.2: {} + fast-glob@3.3.1: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9827,6 +10528,8 @@ snapshots: fresh@2.0.0: {} + fs-constants@1.0.0: {} + fs-extra@11.3.4: dependencies: graceful-fs: 4.2.11 @@ -9882,6 +10585,8 @@ snapshots: get-port-please@3.2.0: {} + get-port@7.2.0: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -9914,6 +10619,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + globals@14.0.0: {} globals@16.4.0: {} @@ -10005,6 +10719,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -10186,6 +10902,8 @@ snapshots: dependencies: is-inside-container: 1.0.0 + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -10214,6 +10932,12 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jiti@2.6.1: {} jose@6.2.3: {} @@ -10299,6 +11023,10 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -10359,8 +11087,12 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: {} + lodash.merge@4.6.2: {} + lodash@4.18.1: {} + log-symbols@6.0.0: dependencies: chalk: 5.6.2 @@ -10372,6 +11104,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + lru-cache@10.4.3: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -10437,8 +11171,22 @@ snapshots: dependencies: brace-expansion: 1.1.14 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.0 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.0 + minimist@1.2.8: {} + minipass@7.1.3: {} + + mkdirp-classic@0.5.3: {} + + mkdirp@3.0.1: {} + ms@2.1.3: {} msw@2.14.2(@types/node@25.6.0)(typescript@6.0.3): @@ -10484,6 +11232,9 @@ snapshots: dependencies: lru.min: 1.1.4 + nan@2.26.2: + optional: true + nanoid@3.3.11: {} nanoid@3.3.12: {} @@ -10542,6 +11293,8 @@ snapshots: non-error@0.1.0: {} + normalize-path@3.0.0: {} + npm-run-path@4.0.1: dependencies: path-key: 3.1.1 @@ -10661,6 +11414,8 @@ snapshots: dependencies: p-limit: 3.1.0 + package-json-from-dist@1.0.1: {} + pako@2.1.0: {} parent-module@1.0.1: @@ -10690,6 +11445,11 @@ snapshots: path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + path-to-regexp@6.3.0: {} path-to-regexp@8.4.2: {} @@ -10837,6 +11597,10 @@ snapshots: - react - react-dom + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -10854,11 +11618,38 @@ snapshots: retry: 0.12.0 signal-exit: 3.0.7 + properties-reader@3.0.1: + dependencies: + '@kwsites/file-exists': 1.1.1 + mkdirp: 3.0.1 + transitivePeerDependencies: + - supports-color + + protobufjs@7.5.6: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.1 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.1 + '@types/node': 25.6.0 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} pure-rand@6.1.0: {} @@ -11008,6 +11799,34 @@ snapshots: react@19.2.5: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + 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 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.9 + readdirp@5.0.0: {} recast@0.23.11: @@ -11159,6 +11978,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safe-push-apply@1.0.0: @@ -11364,10 +12185,25 @@ snapshots: source-map@0.6.1: {} + split-ca@1.0.1: {} + split2@4.2.0: {} sqlstring@2.3.3: {} + ssh-remote-port-forward@1.0.4: + dependencies: + '@types/ssh2': 0.5.52 + ssh2: 1.17.0 + + ssh2@1.17.0: + dependencies: + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + optionalDependencies: + cpu-features: 0.0.10 + nan: 2.26.2 + stable-hash@0.0.5: {} stackback@0.0.2: {} @@ -11388,6 +12224,15 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + streamx@2.25.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + strict-event-emitter@0.5.1: {} string-width@4.2.3: @@ -11396,6 +12241,12 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + string-width@7.2.0: dependencies: emoji-regex: 10.6.0 @@ -11452,6 +12303,14 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + stringify-object@5.0.0: dependencies: get-own-enumerable-keys: 1.0.0 @@ -11500,6 +12359,80 @@ snapshots: tapable@2.3.3: {} + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-fs@3.1.2: + dependencies: + pump: 3.0.4 + tar-stream: 3.2.0 + optionalDependencies: + bare-fs: 4.7.1 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar-stream@3.2.0: + dependencies: + b4a: 1.8.1 + bare-fs: 4.7.1 + fast-fifo: 1.3.2 + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + teex@1.0.1: + dependencies: + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + testcontainers@11.14.0: + dependencies: + '@balena/dockerignore': 1.0.2 + '@types/dockerode': 4.0.1 + archiver: 7.0.1 + async-lock: 1.4.1 + byline: 5.0.0 + debug: 4.4.3 + docker-compose: 1.4.2 + dockerode: 4.0.12 + get-port: 7.2.0 + proper-lockfile: 4.1.2 + properties-reader: 3.0.1 + ssh-remote-port-forward: 1.0.4 + tar-fs: 3.1.2 + tmp: 0.2.5 + undici: 7.25.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + + text-decoder@1.2.7: + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + text-segmentation@1.0.3: dependencies: utrie: 1.0.2 @@ -11524,6 +12457,8 @@ snapshots: dependencies: tldts-core: 7.0.30 + tmp@0.2.5: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -11566,6 +12501,8 @@ snapshots: tw-animate-css@1.4.0: {} + tweetnacl@0.14.5: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -11633,8 +12570,12 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + undici-types@5.26.5: {} + undici-types@7.19.2: {} + undici@7.25.0: {} + unicorn-magic@0.3.0: {} universalify@2.0.1: {} @@ -11703,6 +12644,8 @@ snapshots: base64-arraybuffer: 1.0.2 optional: true + uuid@10.0.0: {} + valibot@1.2.0(typescript@6.0.3): optionalDependencies: typescript: 6.0.3 @@ -11728,7 +12671,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0): + vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.4): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) @@ -11741,11 +12684,12 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.32.0 + yaml: 2.8.4 - vitest@4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(msw@2.14.2(@types/node@25.6.0)(typescript@6.0.3))(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)): + vitest@4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(msw@2.14.2(@types/node@25.6.0)(typescript@6.0.3))(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.4)): dependencies: '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(msw@2.14.2(@types/node@25.6.0)(typescript@6.0.3))(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)) + '@vitest/mocker': 4.1.5(msw@2.14.2(@types/node@25.6.0)(typescript@6.0.3))(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.4)) '@vitest/pretty-format': 4.1.5 '@vitest/runner': 4.1.5 '@vitest/snapshot': 4.1.5 @@ -11762,7 +12706,7 @@ snapshots: tinyexec: 1.1.1 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.4) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.6.0 @@ -11844,6 +12788,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + wrappy@1.0.2: {} wsl-utils@0.3.1: @@ -11857,6 +12807,8 @@ snapshots: yallist@3.1.1: {} + yaml@2.8.4: {} + yargs-parser@21.1.1: {} yargs@17.7.2: @@ -11882,6 +12834,12 @@ snapshots: grammex: 3.1.12 graphmatch: 1.1.1 + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0 + zod-to-json-schema@3.25.2(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/tests/integration/auth-flow.test.ts b/tests/integration/auth-flow.test.ts new file mode 100644 index 0000000..6ee0478 --- /dev/null +++ b/tests/integration/auth-flow.test.ts @@ -0,0 +1,119 @@ +/** + * Integration regression guard for `src/lib/auth/session.ts`. + * + * The session helpers couple Postgres (`Session` row), the `cookies()` + * adapter from `next/headers`, and the `db-compat` schema-migration + * fallback. A unit test with a mocked Prisma client cannot verify the + * end-to-end flow — particularly the expired-session purge behaviour, + * which silently deletes the row before returning null. + */ +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; + +import { + getPrismaClient, + startTestDb, + stopTestDb, + truncateAllTables, +} from "./setup"; + +// Stateful in-memory cookie jar so `createSession()` (which calls +// cookies().set) hands the value off to a subsequent `getSession()` +// (which calls cookies().get), exactly as the request lifecycle does. +const cookieJar = new Map(); + +vi.mock("next/headers", () => ({ + headers: vi.fn(async () => ({ get: () => null })), + cookies: vi.fn(async () => ({ + get: (name: string) => { + const value = cookieJar.get(name); + return value ? { name, value } : undefined; + }, + set: (name: string, value: string) => { + cookieJar.set(name, value); + }, + delete: (name: string) => { + cookieJar.delete(name); + }, + })), +})); + +// `db-compat` runs ALTER TABLE IF NOT EXISTS statements that are no-ops +// against a freshly-migrated schema but slow tests down — short-circuit. +vi.mock("@/lib/db-compat", () => ({ + ensureDbCompatibility: vi.fn().mockResolvedValue(undefined), +})); + +beforeAll(async () => { + await startTestDb(); +}); + +afterAll(async () => { + await stopTestDb(); +}); + +beforeEach(async () => { + await truncateAllTables(getPrismaClient()); + cookieJar.clear(); +}); + +describe("session lifecycle (real Postgres)", () => { + it("createSession writes a row and getSession reads it back", async () => { + const { createSession, getSession } = await import("@/lib/auth/session"); + + const user = await getPrismaClient().user.create({ + data: { username: "session-test", email: "session@example.test" }, + }); + + const sessionId = await createSession(user.id, "127.0.0.1", "vitest/1.0"); + + const row = await getPrismaClient().session.findUnique({ + where: { id: sessionId }, + }); + expect(row).not.toBeNull(); + expect(row?.userId).toBe(user.id); + expect(row?.ipAddress).toBe("127.0.0.1"); + expect(row?.userAgent).toBe("vitest/1.0"); + + const result = await getSession(); + expect(result).not.toBeNull(); + expect(result?.user.id).toBe(user.id); + expect(result?.session.id).toBe(sessionId); + }); + + it("rejects expired sessions and purges the stale row", async () => { + const { createSession, getSession } = await import("@/lib/auth/session"); + + const user = await getPrismaClient().user.create({ + data: { username: "expired-session", email: "expired@example.test" }, + }); + + const sessionId = await createSession(user.id); + + // Force the row past its expiry — getSession must purge it. + await getPrismaClient().session.update({ + where: { id: sessionId }, + data: { expiresAt: new Date(Date.now() - 60_000) }, + }); + + const result = await getSession(); + expect(result).toBeNull(); + + const purged = await getPrismaClient().session.findUnique({ + where: { id: sessionId }, + }); + expect(purged).toBeNull(); + }); + + it("returns null when no session cookie is present", async () => { + const { getSession } = await import("@/lib/auth/session"); + expect(await getSession()).toBeNull(); + }); +}); diff --git a/tests/integration/cascade-delete.test.ts b/tests/integration/cascade-delete.test.ts new file mode 100644 index 0000000..73c249c --- /dev/null +++ b/tests/integration/cascade-delete.test.ts @@ -0,0 +1,203 @@ +/** + * Integration regression guard for the GDPR Art. 17 erasure path. + * + * Deleting a user must wipe every personal-data row that references + * them. The schema declares `onDelete: Cascade` for personal data and + * `onDelete: SetNull` for audit-style rows (AuditLog, Feedback). This + * test verifies the actual database FKs match those declarations — a + * missing CASCADE would leave orphan rows behind, violating GDPR. + */ +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +import { + getPrismaClient, + startTestDb, + stopTestDb, + truncateAllTables, +} from "./setup"; + +beforeAll(async () => { + await startTestDb(); +}); + +afterAll(async () => { + await stopTestDb(); +}); + +beforeEach(async () => { + await truncateAllTables(getPrismaClient()); +}); + +describe("user.delete cascades to all personal-data tables", () => { + it("removes every personal-data row when the user is deleted", async () => { + const prisma = getPrismaClient(); + + const user = await prisma.user.create({ + data: { + username: "erasure-target", + email: "erasure@example.test", + }, + }); + + // Seed personal data across every cascading relation. + await prisma.session.create({ + data: { + userId: user.id, + expiresAt: new Date(Date.now() + 60_000), + }, + }); + + await prisma.passkey.create({ + data: { + userId: user.id, + credentialId: "cred-1", + credentialPublicKey: Buffer.from("pubkey"), + counter: BigInt(0), + credentialDeviceType: "singleDevice", + transports: ["internal"], + }, + }); + + await prisma.measurement.create({ + data: { + userId: user.id, + type: "WEIGHT", + value: 80, + unit: "kg", + measuredAt: new Date(), + }, + }); + + const med = await prisma.medication.create({ + data: { userId: user.id, name: "Aspirin", dose: "100mg" }, + }); + + await prisma.medicationIntakeEvent.create({ + data: { + userId: user.id, + medicationId: med.id, + scheduledFor: new Date(), + }, + }); + + await prisma.moodEntry.create({ + data: { + userId: user.id, + date: "2026-05-08", + mood: "GUT", + score: 4, + moodLoggedAt: new Date(), + }, + }); + + await prisma.apiToken.create({ + data: { + userId: user.id, + name: "test-token", + tokenHash: "hash-" + user.id, + }, + }); + + await prisma.dataBackup.create({ + data: { userId: user.id, type: "MANUAL", data: "{}" }, + }); + + await prisma.userAchievement.create({ + data: { + userId: user.id, + achievementId: "first-login", + unlockedAt: new Date(), + }, + }); + + await prisma.idempotencyKey.create({ + data: { + userId: user.id, + key: "abcdef12345678", + method: "POST", + path: "/api/test", + responseStatus: 200, + responseBody: "{}", + expiresAt: new Date(Date.now() + 60_000), + }, + }); + + await prisma.device.create({ + data: { + userId: user.id, + platform: "ios", + token: "apns-token-" + user.id, + bundleId: "app.healthlog.ios", + }, + }); + + await prisma.pushSubscription.create({ + data: { + userId: user.id, + endpoint: "https://push.example/sub-" + user.id, + p256dh: "p256dh", + auth: "auth", + }, + }); + + // Audit logs / feedback are intentionally SetNull, not Cascade — + // they survive deletion with a null userId for compliance triage. + const auditRow = await prisma.auditLog.create({ + data: { userId: user.id, action: "auth.login" }, + }); + const feedbackRow = await prisma.feedback.create({ + data: { + userId: user.id, + category: "BUG", + subject: "test", + description: "test", + }, + }); + + // ── act ── + await prisma.user.delete({ where: { id: user.id } }); + + // ── assert: every cascading table is empty for that user id ── + expect(await prisma.session.count({ where: { userId: user.id } })).toBe(0); + expect(await prisma.passkey.count({ where: { userId: user.id } })).toBe(0); + expect(await prisma.measurement.count({ where: { userId: user.id } })).toBe( + 0, + ); + expect(await prisma.medication.count({ where: { userId: user.id } })).toBe( + 0, + ); + expect( + await prisma.medicationIntakeEvent.count({ + where: { userId: user.id }, + }), + ).toBe(0); + expect(await prisma.moodEntry.count({ where: { userId: user.id } })).toBe( + 0, + ); + expect(await prisma.apiToken.count({ where: { userId: user.id } })).toBe(0); + expect(await prisma.dataBackup.count({ where: { userId: user.id } })).toBe( + 0, + ); + expect( + await prisma.userAchievement.count({ where: { userId: user.id } }), + ).toBe(0); + expect( + await prisma.idempotencyKey.count({ where: { userId: user.id } }), + ).toBe(0); + expect(await prisma.device.count({ where: { userId: user.id } })).toBe(0); + expect( + await prisma.pushSubscription.count({ where: { userId: user.id } }), + ).toBe(0); + + // SetNull rows survive but lose their userId. + const auditAfter = await prisma.auditLog.findUnique({ + where: { id: auditRow.id }, + }); + expect(auditAfter?.userId).toBeNull(); + + const feedbackAfter = await prisma.feedback.findUnique({ + where: { id: feedbackRow.id }, + }); + expect(feedbackAfter?.userId).toBeNull(); + }); +}); diff --git a/tests/integration/idempotency-replay.test.ts b/tests/integration/idempotency-replay.test.ts new file mode 100644 index 0000000..fd3ab61 --- /dev/null +++ b/tests/integration/idempotency-replay.test.ts @@ -0,0 +1,180 @@ +/** + * Integration regression guard for `src/lib/idempotency.ts`. + * + * Mobile clients retry POST/PUT/PATCH/DELETE with the same + * `Idempotency-Key`; the wrapper must replay the cached response without + * re-running the side-effect. These tests exercise the cache table + * (`idempotency_keys`) end-to-end against a real Postgres. + */ +import { NextRequest, NextResponse } from "next/server"; +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; + +import { + getPrismaClient, + startTestDb, + stopTestDb, + truncateAllTables, +} from "./setup"; + +// `withIdempotency()` calls `getSession()` (cookie-backed) by default and +// falls back to a Bearer-token lookup. We stub `next/headers` to return +// a fixed user id via the cookie path so the integration stays focused +// on the cache-replay contract rather than the auth machinery. +const TEST_USER_ID = "user-idempotency-test"; + +vi.mock("next/headers", () => ({ + headers: vi.fn(async () => ({ get: () => null })), + cookies: vi.fn(async () => ({ + get: () => undefined, + set: () => {}, + delete: () => {}, + })), +})); + +vi.mock("@/lib/auth/session", () => ({ + getSession: vi.fn(async () => ({ + session: { + id: "session-stub", + expiresAt: new Date(Date.now() + 1_000_000), + }, + user: { id: TEST_USER_ID }, + })), +})); + +vi.mock("@/lib/db-compat", () => ({ + ensureDbCompatibility: vi.fn().mockResolvedValue(undefined), +})); + +beforeAll(async () => { + await startTestDb(); +}); + +afterAll(async () => { + await stopTestDb(); +}); + +beforeEach(async () => { + await truncateAllTables(getPrismaClient()); + // Seed the user the resolver claims to authenticate as so the FK + // constraint on idempotency_keys.user_id holds. + await getPrismaClient().user.create({ + data: { + id: TEST_USER_ID, + username: "idempotency-test", + email: "idempotency@example.test", + }, + }); +}); + +function makeRequest(key: string | null, path = "/api/test"): NextRequest { + const headers: Record = { + "content-type": "application/json", + }; + if (key) headers["idempotency-key"] = key; + return new NextRequest(`http://localhost${path}`, { + method: "POST", + headers, + body: JSON.stringify({ ok: true }), + }); +} + +describe("withIdempotency (real Postgres)", () => { + it("replays the cached response and skips the handler on retry", async () => { + const { withIdempotency } = await import("@/lib/idempotency"); + + let invocations = 0; + const handler = withIdempotency(async () => { + invocations++; + return NextResponse.json({ count: invocations }, { status: 201 }); + }); + + const first = await handler(makeRequest("abc-12345678")); + expect(first.status).toBe(201); + expect(await first.json()).toEqual({ count: 1 }); + expect(invocations).toBe(1); + + const replay = await handler(makeRequest("abc-12345678")); + expect(replay.status).toBe(201); + expect(replay.headers.get("X-Idempotent-Replay")).toBe("true"); + expect(await replay.json()).toEqual({ count: 1 }); + expect(invocations).toBe(1); // handler did NOT run again + + const rows = await getPrismaClient().idempotencyKey.findMany({ + where: { userId: TEST_USER_ID, key: "abc-12345678" }, + }); + expect(rows).toHaveLength(1); + }); + + it("re-runs the handler after the cached row's TTL has elapsed", async () => { + const { withIdempotency } = await import("@/lib/idempotency"); + + let invocations = 0; + const handler = withIdempotency(async () => { + invocations++; + return NextResponse.json({ count: invocations }, { status: 201 }); + }); + + await handler(makeRequest("expired-12345678")); + expect(invocations).toBe(1); + + // Force the cached row past its expiry — the wrapper purges and + // falls through to the handler. + await getPrismaClient().idempotencyKey.updateMany({ + where: { userId: TEST_USER_ID, key: "expired-12345678" }, + data: { expiresAt: new Date(Date.now() - 1_000) }, + }); + + const second = await handler(makeRequest("expired-12345678")); + expect(second.status).toBe(201); + expect(await second.json()).toEqual({ count: 2 }); + expect(invocations).toBe(2); + + const rows = await getPrismaClient().idempotencyKey.findMany({ + where: { userId: TEST_USER_ID, key: "expired-12345678" }, + }); + // Original row was deleted as stale, then a fresh one was inserted. + expect(rows).toHaveLength(1); + expect(rows[0]?.expiresAt.getTime()).toBeGreaterThan(Date.now()); + }); + + it("does NOT cache 401/403/408/429/5xx responses", async () => { + const { withIdempotency } = await import("@/lib/idempotency"); + + const cases = [401, 403, 408, 429, 500, 502, 503]; + for (const status of cases) { + const key = `noncacheable-${status}-abcdef`; + const handler = withIdempotency(async () => + NextResponse.json({ status }, { status }), + ); + const response = await handler(makeRequest(key)); + expect(response.status).toBe(status); + + const row = await getPrismaClient().idempotencyKey.findFirst({ + where: { userId: TEST_USER_ID, key }, + }); + expect(row).toBeNull(); + } + }); + + it("DOES cache 4xx-validation responses (e.g. 422)", async () => { + const { withIdempotency } = await import("@/lib/idempotency"); + + const handler = withIdempotency(async () => + NextResponse.json({ error: "validation" }, { status: 422 }), + ); + + await handler(makeRequest("validation-12345678")); + const row = await getPrismaClient().idempotencyKey.findFirst({ + where: { userId: TEST_USER_ID, key: "validation-12345678" }, + }); + expect(row?.responseStatus).toBe(422); + }); +}); diff --git a/tests/integration/rate-limit.test.ts b/tests/integration/rate-limit.test.ts new file mode 100644 index 0000000..2f8541d --- /dev/null +++ b/tests/integration/rate-limit.test.ts @@ -0,0 +1,88 @@ +/** + * Integration regression guard for `src/lib/rate-limit.ts`. + * + * The limiter relies on a single atomic SQL upsert (`INSERT ... ON + * CONFLICT DO UPDATE`) so concurrent calls cannot exceed the cap. These + * tests prove that contract against a real Postgres — a unit test with + * a mocked client could not detect a missing UPSERT or a misplaced + * window-reset branch. + */ +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +import { + getPrismaClient, + startTestDb, + stopTestDb, + truncateAllTables, +} from "./setup"; + +beforeAll(async () => { + await startTestDb(); +}); + +afterAll(async () => { + await stopTestDb(); +}); + +beforeEach(async () => { + await truncateAllTables(getPrismaClient()); +}); + +describe("checkRateLimit (real Postgres)", () => { + it("permits exactly `limit` of N concurrent calls in the same window", async () => { + // Lazy import so `process.env.DATABASE_URL` is set by startTestDb() + // *before* the module pulls in `@/lib/db`. + const { checkRateLimit } = await import("@/lib/rate-limit"); + + const key = "test:concurrent:1.2.3.4"; + const limit = 5; + const windowMs = 60_000; + + const results = await Promise.all( + Array.from({ length: 6 }, () => checkRateLimit(key, limit, windowMs)), + ); + + const allowed = results.filter((r) => r.allowed).length; + const denied = results.filter((r) => !r.allowed).length; + + expect(allowed).toBe(5); + expect(denied).toBe(1); + + const row = await getPrismaClient().rateLimit.findUnique({ + where: { key }, + }); + expect(row?.count).toBe(6); + }); + + it("resets the counter once the window expires", async () => { + const { checkRateLimit } = await import("@/lib/rate-limit"); + + const key = "test:reset:1.2.3.4"; + const limit = 3; + + // Burn the budget with a short 50ms window. + for (let i = 0; i < 3; i++) { + const r = await checkRateLimit(key, limit, 50); + expect(r.allowed).toBe(true); + } + const denied = await checkRateLimit(key, limit, 50); + expect(denied.allowed).toBe(false); + + // Manually expire the window: cheaper than sleeping and avoids + // flaky timing on slow CI runners. The branch under test compares + // `reset_at < NOW()`, so any past timestamp triggers the reset. + await getPrismaClient().rateLimit.update({ + where: { key }, + data: { resetAt: new Date(Date.now() - 1_000) }, + }); + + const afterReset = await checkRateLimit(key, limit, 60_000); + expect(afterReset.allowed).toBe(true); + + const row = await getPrismaClient().rateLimit.findUnique({ + where: { key }, + }); + expect(row?.count).toBe(1); + expect(row?.resetAt.getTime()).toBeGreaterThan(Date.now()); + }); +}); diff --git a/tests/integration/setup.ts b/tests/integration/setup.ts new file mode 100644 index 0000000..1a6ac1f --- /dev/null +++ b/tests/integration/setup.ts @@ -0,0 +1,156 @@ +/** + * Integration test bootstrap — boots a Postgres testcontainer, runs the + * project's Prisma migrations against it, and exposes a singleton Prisma + * client. Each test file should: + * + * import { startTestDb, stopTestDb, getPrismaClient, truncateAllTables } + * from "./setup"; + * + * beforeAll(async () => { await startTestDb(); }); + * afterAll(async () => { await stopTestDb(); }); + * beforeEach(async () => { await truncateAllTables(getPrismaClient()); }); + * + * The container is reused across tests inside the same vitest worker + * (singleFork is enabled in vitest.integration.config.mts) — booting once + * keeps the suite well under the 60s test timeout. + */ +import { execSync } from "node:child_process"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { PrismaPg } from "@prisma/adapter-pg"; +import { + PostgreSqlContainer, + type StartedPostgreSqlContainer, +} from "@testcontainers/postgresql"; + +import { PrismaClient } from "@/generated/prisma/client"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const PROJECT_ROOT = resolve(__dirname, "..", ".."); + +let container: StartedPostgreSqlContainer | null = null; +let prisma: PrismaClient | null = null; +let originalDatabaseUrl: string | undefined; + +/** + * Start a fresh Postgres container, point Prisma at it, and apply all + * migrations. Idempotent within a vitest worker — repeated calls return + * the already-running container. + */ +export async function startTestDb(): Promise { + if (container && prisma) return; + + container = await new PostgreSqlContainer("postgres:16-alpine") + .withDatabase("healthlog_test") + .withUsername("healthlog") + .withPassword("healthlog") + .start(); + + // Prisma 7 + adapter-pg can read the URL from process.env.DATABASE_URL + // when prisma migrate deploy is invoked from a child process. The + // ?schema=public&pgbouncer=false hints keep behaviour explicit and + // avoid surprises when running against pgbouncer in production. + const url = container.getConnectionUri() + "?schema=public&pgbouncer=false"; + originalDatabaseUrl = process.env.DATABASE_URL; + process.env.DATABASE_URL = url; + + // Apply migrations in a child process so Prisma's CLI machinery picks + // up the freshly-set DATABASE_URL and uses the project's prisma.config.ts. + execSync("pnpm db:migrate:deploy", { + cwd: PROJECT_ROOT, + stdio: "inherit", + env: { ...process.env, DATABASE_URL: url }, + }); + + const adapter = new PrismaPg({ connectionString: url }); + prisma = new PrismaClient({ adapter }); + await prisma.$connect(); +} + +/** + * Stop the Postgres container and disconnect the singleton Prisma client. + * Restores the original DATABASE_URL so a follow-up suite in the same + * process doesn't accidentally reuse the dead container's URL. + */ +export async function stopTestDb(): Promise { + try { + if (prisma) { + await prisma.$disconnect(); + } + } finally { + prisma = null; + } + + try { + if (container) { + await container.stop({ remove: true }); + } + } finally { + container = null; + } + + if (originalDatabaseUrl === undefined) { + delete process.env.DATABASE_URL; + } else { + process.env.DATABASE_URL = originalDatabaseUrl; + } + originalDatabaseUrl = undefined; +} + +/** + * Singleton Prisma client connected to the running testcontainer. Throws + * if `startTestDb()` has not been awaited. + */ +export function getPrismaClient(): PrismaClient { + if (!prisma) { + throw new Error( + "Prisma client is not initialised — call startTestDb() in beforeAll() first.", + ); + } + return prisma; +} + +/** + * Truncate every user-facing table in dependency-safe order using + * `TRUNCATE … RESTART IDENTITY CASCADE`. Call from beforeEach() to make + * tests independent. The list mirrors `prisma/schema.prisma` (25 models) + * minus prisma-internal tables (_prisma_migrations). + */ +export async function truncateAllTables(client: PrismaClient): Promise { + // CASCADE means we only need to enumerate the tables; FK chains are + // handled by Postgres. Listed alphabetically for diff stability. + const tables = [ + "api_tokens", + "app_settings", + "audit_logs", + "auth_challenges", + "data_backups", + "devices", + "feedback", + "idempotency_keys", + "measurements", + "medication_intake_events", + "medication_schedules", + "medications", + "mood_entries", + "notification_channels", + "notification_preferences", + "passkeys", + "push_subscriptions", + "rate_limits", + "reminder_phase_configs", + "sessions", + "telegram_reminder_messages", + "telegram_scheduled_deletions", + "user_achievements", + "users", + "withings_connections", + ]; + + const quoted = tables.map((t) => `"${t}"`).join(", "); + await client.$executeRawUnsafe( + `TRUNCATE TABLE ${quoted} RESTART IDENTITY CASCADE;`, + ); +} diff --git a/vitest.integration.config.mts b/vitest.integration.config.mts new file mode 100644 index 0000000..13143aa --- /dev/null +++ b/vitest.integration.config.mts @@ -0,0 +1,33 @@ +import { defineConfig } from "vitest/config"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Integration suite — boots a real Postgres in a testcontainer and runs +// migrations against it. Slow (container boot ~5–15s on a cold pull), +// so excluded from the default `pnpm test` and triggered explicitly via +// `pnpm test:integration`. +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["tests/integration/**/*.test.ts"], + // Container boot + migration apply is slow; give each test a generous + // budget and beforeAll/afterAll twice that. + testTimeout: 60_000, + hookTimeout: 120_000, + // Run sequentially in a single fork so files reuse one container and + // amortise the migration-apply cost. Vitest 4 moved these to the top + // level (the old `poolOptions.forks.singleFork` was removed). + pool: "forks", + fileParallelism: false, + isolate: false, + }, + resolve: { + alias: { + "@": resolve(__dirname, "src"), + }, + }, +}); From d4aba697cfadcf6f00d7227ddfffd1709d283cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 8 May 2026 14:31:38 +0200 Subject: [PATCH 2/3] test(integration): rebase onto v1.4 + exclude live agent worktrees from default test scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebase brings PR #137's testcontainers infrastructure on top of the user-locale migration drift fix landed in #140 — the integration suite should now bootstrap a fresh Postgres container with all 25 migrations applied cleanly. vitest.config.mts also gains a `.claude/worktrees/**` exclude so live agent worktrees (which mirror `src/` while their parent agents are running) don't double-execute the project's tests against possibly-stale copies. --- vitest.config.mts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/vitest.config.mts b/vitest.config.mts index f844840..e71d0c0 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -11,7 +11,15 @@ export default defineConfig({ environment: "node", // Default `pnpm test` keeps unit tests only — integration suite uses // testcontainers and runs separately via `pnpm test:integration`. - exclude: ["**/node_modules/**", "**/dist/**", "tests/integration/**"], + exclude: [ + "**/node_modules/**", + "**/dist/**", + "tests/integration/**", + // Live agent worktrees create copies of `src/` under + // `.claude/worktrees/`; vitest would otherwise pick those copies up + // and run their tests twice — possibly against stale snapshots. + ".claude/worktrees/**", + ], coverage: { provider: "v8", // `include` enumerates the full source tree so the report shows From 0656694bcd922202c073b31e7fb415f2ff1ef2a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 8 May 2026 16:02:54 +0200 Subject: [PATCH 3/3] test(integration): boot one Postgres container per run via vitest globalSetup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first iteration of the testcontainers suite started/stopped a fresh container in each test file's beforeAll/afterAll. That works in isolation but breaks when files share a worker (`pool: forks`, `fileParallelism: false`, `isolate: false`): 1. Test file A boots container 1, sets DATABASE_URL. 2. The application's Prisma singleton in src/lib/db.ts is built when test code dynamic-imports @/lib/auth/session — it captures container 1's URL. 3. afterAll stops container 1. 4. Test file B boots container 2, sets DATABASE_URL to a new port. 5. Test file B imports application code that resolves to the same singleton — still pointing at the dead container 1. Queries fail with a generic PrismaClientKnownRequestError. Fix: move the container lifecycle to vitest's globalSetup. One container, booted once, torn down at the end of the run. Tests still get isolation because beforeEach truncates every personal-data table in dependency-safe CASCADE order. - tests/integration/global-setup.ts boots postgres:16-alpine, runs `pnpm db:migrate:deploy`, exports a teardown for end-of-run. - tests/integration/setup.ts now exports getPrismaClient() (which delegates to the application singleton) and truncateAllTables(). No more startTestDb / stopTestDb. - All four test files lose their beforeAll/afterAll boilerplate. - vitest.integration.config.mts wires the new globalSetup. - truncate list adds refresh_tokens (added in 0025_refresh_tokens). Result: pnpm test:integration → 10/10 pass in 3.9s (was 6/10 with 4 cross-file failures). pnpm test → 658/658 unchanged. Co-Authored-By: Marc-André Bombeck --- tests/integration/auth-flow.test.ts | 27 +---- tests/integration/cascade-delete.test.ts | 17 +-- tests/integration/global-setup.ts | 57 +++++++++ tests/integration/idempotency-replay.test.ts | 27 +---- tests/integration/rate-limit.test.ts | 17 +-- tests/integration/setup.ts | 121 +++---------------- vitest.integration.config.mts | 2 + 7 files changed, 87 insertions(+), 181 deletions(-) create mode 100644 tests/integration/global-setup.ts diff --git a/tests/integration/auth-flow.test.ts b/tests/integration/auth-flow.test.ts index 6ee0478..0207ab3 100644 --- a/tests/integration/auth-flow.test.ts +++ b/tests/integration/auth-flow.test.ts @@ -7,22 +7,9 @@ * end-to-end flow — particularly the expired-session purge behaviour, * which silently deletes the row before returning null. */ -import { - afterAll, - beforeAll, - beforeEach, - describe, - expect, - it, - vi, -} from "vitest"; - -import { - getPrismaClient, - startTestDb, - stopTestDb, - truncateAllTables, -} from "./setup"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { getPrismaClient, truncateAllTables } from "./setup"; // Stateful in-memory cookie jar so `createSession()` (which calls // cookies().set) hands the value off to a subsequent `getSession()` @@ -51,14 +38,6 @@ vi.mock("@/lib/db-compat", () => ({ ensureDbCompatibility: vi.fn().mockResolvedValue(undefined), })); -beforeAll(async () => { - await startTestDb(); -}); - -afterAll(async () => { - await stopTestDb(); -}); - beforeEach(async () => { await truncateAllTables(getPrismaClient()); cookieJar.clear(); diff --git a/tests/integration/cascade-delete.test.ts b/tests/integration/cascade-delete.test.ts index 73c249c..456c5bc 100644 --- a/tests/integration/cascade-delete.test.ts +++ b/tests/integration/cascade-delete.test.ts @@ -7,22 +7,9 @@ * test verifies the actual database FKs match those declarations — a * missing CASCADE would leave orphan rows behind, violating GDPR. */ -import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; -import { - getPrismaClient, - startTestDb, - stopTestDb, - truncateAllTables, -} from "./setup"; - -beforeAll(async () => { - await startTestDb(); -}); - -afterAll(async () => { - await stopTestDb(); -}); +import { getPrismaClient, truncateAllTables } from "./setup"; beforeEach(async () => { await truncateAllTables(getPrismaClient()); diff --git a/tests/integration/global-setup.ts b/tests/integration/global-setup.ts new file mode 100644 index 0000000..c460a98 --- /dev/null +++ b/tests/integration/global-setup.ts @@ -0,0 +1,57 @@ +/** + * Vitest globalSetup — boots ONE Postgres testcontainer for the whole + * integration run, applies migrations, and exposes the connection URL + * via process.env.DATABASE_URL. The teardown returned at the end stops + * the container after all test files have finished. + * + * Why globalSetup instead of beforeAll/afterAll per file: + * - The application's Prisma singleton in `src/lib/db.ts` is built at + * module load time using the current `process.env.DATABASE_URL`. If + * the container is rotated between test files, the singleton keeps + * pointing at the dead container and queries fail. Booting once, + * keeping the URL stable, and truncating between tests is both + * faster and safer. + * - Truncate-between-tests still gives per-test isolation; see + * `setup.ts -> truncateAllTables`. + */ +import { execSync } from "node:child_process"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + PostgreSqlContainer, + type StartedPostgreSqlContainer, +} from "@testcontainers/postgresql"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const PROJECT_ROOT = resolve(__dirname, "..", ".."); + +let container: StartedPostgreSqlContainer | null = null; + +export default async function setup(): Promise<() => Promise> { + container = await new PostgreSqlContainer("postgres:16-alpine") + .withDatabase("healthlog_test") + .withUsername("healthlog") + .withPassword("healthlog") + .start(); + + const url = container.getConnectionUri() + "?schema=public&pgbouncer=false"; + process.env.DATABASE_URL = url; + + // Provided so child processes (e.g. Prisma's CLI) inherit the URL. + // Vitest also forwards process.env to test workers, so the + // application Prisma singleton sees this URL when it loads. + execSync("pnpm db:migrate:deploy", { + cwd: PROJECT_ROOT, + stdio: "inherit", + env: { ...process.env, DATABASE_URL: url }, + }); + + return async () => { + if (container) { + await container.stop({ remove: true }); + container = null; + } + }; +} diff --git a/tests/integration/idempotency-replay.test.ts b/tests/integration/idempotency-replay.test.ts index fd3ab61..04a0f72 100644 --- a/tests/integration/idempotency-replay.test.ts +++ b/tests/integration/idempotency-replay.test.ts @@ -7,22 +7,9 @@ * (`idempotency_keys`) end-to-end against a real Postgres. */ import { NextRequest, NextResponse } from "next/server"; -import { - afterAll, - beforeAll, - beforeEach, - describe, - expect, - it, - vi, -} from "vitest"; - -import { - getPrismaClient, - startTestDb, - stopTestDb, - truncateAllTables, -} from "./setup"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { getPrismaClient, truncateAllTables } from "./setup"; // `withIdempotency()` calls `getSession()` (cookie-backed) by default and // falls back to a Bearer-token lookup. We stub `next/headers` to return @@ -53,14 +40,6 @@ vi.mock("@/lib/db-compat", () => ({ ensureDbCompatibility: vi.fn().mockResolvedValue(undefined), })); -beforeAll(async () => { - await startTestDb(); -}); - -afterAll(async () => { - await stopTestDb(); -}); - beforeEach(async () => { await truncateAllTables(getPrismaClient()); // Seed the user the resolver claims to authenticate as so the FK diff --git a/tests/integration/rate-limit.test.ts b/tests/integration/rate-limit.test.ts index 2f8541d..fa0d3b4 100644 --- a/tests/integration/rate-limit.test.ts +++ b/tests/integration/rate-limit.test.ts @@ -7,22 +7,9 @@ * a mocked client could not detect a missing UPSERT or a misplaced * window-reset branch. */ -import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; -import { - getPrismaClient, - startTestDb, - stopTestDb, - truncateAllTables, -} from "./setup"; - -beforeAll(async () => { - await startTestDb(); -}); - -afterAll(async () => { - await stopTestDb(); -}); +import { getPrismaClient, truncateAllTables } from "./setup"; beforeEach(async () => { await truncateAllTables(getPrismaClient()); diff --git a/tests/integration/setup.ts b/tests/integration/setup.ts index 1a6ac1f..c3992df 100644 --- a/tests/integration/setup.ts +++ b/tests/integration/setup.ts @@ -1,122 +1,36 @@ /** - * Integration test bootstrap — boots a Postgres testcontainer, runs the - * project's Prisma migrations against it, and exposes a singleton Prisma - * client. Each test file should: + * Per-test helpers for the integration suite. The Postgres testcontainer + * is started ONCE in `global-setup.ts` and torn down at the end of the + * run, so test files only need: * - * import { startTestDb, stopTestDb, getPrismaClient, truncateAllTables } - * from "./setup"; + * import { getPrismaClient, truncateAllTables } from "./setup"; * - * beforeAll(async () => { await startTestDb(); }); - * afterAll(async () => { await stopTestDb(); }); * beforeEach(async () => { await truncateAllTables(getPrismaClient()); }); * - * The container is reused across tests inside the same vitest worker - * (singleFork is enabled in vitest.integration.config.mts) — booting once - * keeps the suite well under the 60s test timeout. + * No beforeAll(startTestDb) / afterAll(stopTestDb) is needed — the + * container is alive whenever the test file is loaded, and Vitest's + * worker inherits `process.env.DATABASE_URL` from globalSetup so the + * application's Prisma singleton (`src/lib/db.ts`) connects to the + * testcontainer transparently. */ -import { execSync } from "node:child_process"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; - -import { PrismaPg } from "@prisma/adapter-pg"; -import { - PostgreSqlContainer, - type StartedPostgreSqlContainer, -} from "@testcontainers/postgresql"; - -import { PrismaClient } from "@/generated/prisma/client"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const PROJECT_ROOT = resolve(__dirname, "..", ".."); - -let container: StartedPostgreSqlContainer | null = null; -let prisma: PrismaClient | null = null; -let originalDatabaseUrl: string | undefined; - -/** - * Start a fresh Postgres container, point Prisma at it, and apply all - * migrations. Idempotent within a vitest worker — repeated calls return - * the already-running container. - */ -export async function startTestDb(): Promise { - if (container && prisma) return; - - container = await new PostgreSqlContainer("postgres:16-alpine") - .withDatabase("healthlog_test") - .withUsername("healthlog") - .withPassword("healthlog") - .start(); - - // Prisma 7 + adapter-pg can read the URL from process.env.DATABASE_URL - // when prisma migrate deploy is invoked from a child process. The - // ?schema=public&pgbouncer=false hints keep behaviour explicit and - // avoid surprises when running against pgbouncer in production. - const url = container.getConnectionUri() + "?schema=public&pgbouncer=false"; - originalDatabaseUrl = process.env.DATABASE_URL; - process.env.DATABASE_URL = url; - - // Apply migrations in a child process so Prisma's CLI machinery picks - // up the freshly-set DATABASE_URL and uses the project's prisma.config.ts. - execSync("pnpm db:migrate:deploy", { - cwd: PROJECT_ROOT, - stdio: "inherit", - env: { ...process.env, DATABASE_URL: url }, - }); - - const adapter = new PrismaPg({ connectionString: url }); - prisma = new PrismaClient({ adapter }); - await prisma.$connect(); -} - -/** - * Stop the Postgres container and disconnect the singleton Prisma client. - * Restores the original DATABASE_URL so a follow-up suite in the same - * process doesn't accidentally reuse the dead container's URL. - */ -export async function stopTestDb(): Promise { - try { - if (prisma) { - await prisma.$disconnect(); - } - } finally { - prisma = null; - } - - try { - if (container) { - await container.stop({ remove: true }); - } - } finally { - container = null; - } - - if (originalDatabaseUrl === undefined) { - delete process.env.DATABASE_URL; - } else { - process.env.DATABASE_URL = originalDatabaseUrl; - } - originalDatabaseUrl = undefined; -} +import { prisma } from "@/lib/db"; +import type { PrismaClient } from "@/generated/prisma/client"; /** - * Singleton Prisma client connected to the running testcontainer. Throws - * if `startTestDb()` has not been awaited. + * The application's Prisma singleton. Tests use this so any code + * imported via `await import("@/lib/...")` shares the exact same client + * instance — no risk of a divergent connection pool reading stale + * truncation state. */ export function getPrismaClient(): PrismaClient { - if (!prisma) { - throw new Error( - "Prisma client is not initialised — call startTestDb() in beforeAll() first.", - ); - } return prisma; } /** * Truncate every user-facing table in dependency-safe order using * `TRUNCATE … RESTART IDENTITY CASCADE`. Call from beforeEach() to make - * tests independent. The list mirrors `prisma/schema.prisma` (25 models) - * minus prisma-internal tables (_prisma_migrations). + * tests independent. The list mirrors `prisma/schema.prisma` minus + * prisma-internal tables (_prisma_migrations). */ export async function truncateAllTables(client: PrismaClient): Promise { // CASCADE means we only need to enumerate the tables; FK chains are @@ -140,6 +54,7 @@ export async function truncateAllTables(client: PrismaClient): Promise { "passkeys", "push_subscriptions", "rate_limits", + "refresh_tokens", "reminder_phase_configs", "sessions", "telegram_reminder_messages", diff --git a/vitest.integration.config.mts b/vitest.integration.config.mts index 13143aa..ed3f4eb 100644 --- a/vitest.integration.config.mts +++ b/vitest.integration.config.mts @@ -24,6 +24,8 @@ export default defineConfig({ pool: "forks", fileParallelism: false, isolate: false, + // One container for the whole run; tests truncate between them. + globalSetup: ["./tests/integration/global-setup.ts"], }, resolve: { alias: {