From f17fd3db78a5be7804e70c4c964923a704682e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20P=C3=A1scoa?= Date: Sun, 11 Oct 2026 19:26:22 +0100 Subject: [PATCH] test: add e2e (Playwright) and D1 integration tests, plus CI - tests/e2e: sign-in redirect, signed-in session, groups/expenses/settle-up, stranger blocked, recurring expense, run against the built Worker in parallel - src/tests/integration: authorization, balances, expense atomicity, settlements, recurrence idempotency, D1 query regressions (vitest + @cloudflare/vitest-plugin) - .github/workflows/ci.yml: typecheck, integration, e2e in parallel with caching - replace Jest with Vitest; scenarios adapted from upstream PR oss-apps/split-pro#738 Bugs found by the tests and fixed here: - any signed-in user could read, edit or delete any expense by id, and write to any group - runRecurringJob used db.run() inside a D1 batch, so recurring expenses never ran --- .github/workflows/check.yml | 49 - .github/workflows/ci.yml | 92 + .gitignore | 6 + README.md | 19 +- jest.config.ts | 214 - package.json | 14 +- playwright.config.ts | 21 + pnpm-lock.yaml | 3716 +++-------------- src/lib/cron.test.ts | 65 - src/server/api/routers/expense.ts | 89 +- src/server/api/services/splitService.ts | 53 +- src/tests/addStore.test.ts | 1190 ------ src/tests/currencyPreferenceStore.test.ts | 67 - src/tests/env.test.ts | 27 - src/tests/healthz.test.ts | 37 - .../authorization.integration.test.ts | 251 ++ .../integration/balances.integration.test.ts | 102 + .../integration/expense.integration.test.ts | 174 + src/tests/integration/helpers.ts | 148 + .../integration/queries.integration.test.ts | 138 + .../recurrence.integration.test.ts | 135 + src/tests/mock/example_group_balances.json | 1490 ------- src/tests/number.test.ts | 254 -- src/tests/readyz.test.ts | 118 - src/tests/simplify.test.ts | 210 - test/setup.integration.ts | 17 + test/wrangler.test.jsonc | 20 + tests/e2e/app.spec.ts | 220 + tests/e2e/fixtures.ts | 40 + tests/e2e/prepare.mjs | 36 + tests/e2e/users.mjs | 12 + tsconfig.json | 7 +- vitest.config.ts | 34 + 33 files changed, 2120 insertions(+), 6945 deletions(-) delete mode 100644 .github/workflows/check.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 jest.config.ts create mode 100644 playwright.config.ts delete mode 100644 src/lib/cron.test.ts delete mode 100644 src/tests/addStore.test.ts delete mode 100644 src/tests/currencyPreferenceStore.test.ts delete mode 100644 src/tests/env.test.ts delete mode 100644 src/tests/healthz.test.ts create mode 100644 src/tests/integration/authorization.integration.test.ts create mode 100644 src/tests/integration/balances.integration.test.ts create mode 100644 src/tests/integration/expense.integration.test.ts create mode 100644 src/tests/integration/helpers.ts create mode 100644 src/tests/integration/queries.integration.test.ts create mode 100644 src/tests/integration/recurrence.integration.test.ts delete mode 100644 src/tests/mock/example_group_balances.json delete mode 100644 src/tests/number.test.ts delete mode 100644 src/tests/readyz.test.ts delete mode 100644 src/tests/simplify.test.ts create mode 100644 test/setup.integration.ts create mode 100644 test/wrangler.test.jsonc create mode 100644 tests/e2e/app.spec.ts create mode 100644 tests/e2e/fixtures.ts create mode 100644 tests/e2e/prepare.mjs create mode 100644 tests/e2e/users.mjs create mode 100644 vitest.config.ts diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml deleted file mode 100644 index 2129167ef..000000000 --- a/.github/workflows/check.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Check - -on: - - pull_request -permissions: - contents: read - -jobs: - check: - runs-on: ubuntu-latest - env: - SKIP_ENV_VALIDATION: true - - steps: - - name: Checkout - uses: actions/checkout@v6 - - - uses: pnpm/action-setup@v5 - name: Install pnpm - with: - run_install: false - - - name: Install Node.js - uses: actions/setup-node@v6 - with: - node-version: 22.16.0 - cache: pnpm - - - name: Install dependencies - run: pnpm install - - - name: Apply prettier formatting - run: pnpm prettier --check . - - - name: Run lint - run: pnpm lint - - - name: Run tsc - run: pnpm tsgo --noEmit - - - name: Run tests - run: pnpm test - - # Database seed hash verification is currently disabled because it is failing in CI. - # - name: Verify seed hash - # run: pnpm db:verify-seed-hash - - - name: Run build - run: pnpm build --no-lint diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..8ea861a6a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,92 @@ +name: CI + +on: + push: + branches: [cloudflare-workers, main] + pull_request: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +# Target: the whole workflow finishes in under 2 minutes. The three jobs run in parallel and every +# one of them restores the pnpm store (and, for e2e, the Playwright browsers and the build) from cache. +jobs: + typecheck: + runs-on: ubuntu-latest + timeout-minutes: 5 + 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 --prefer-offline + - run: npx tsc --noEmit -p . + + integration: + runs-on: ubuntu-latest + timeout-minutes: 5 + 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 --prefer-offline + # Real D1 (workerd) with the production migrations: authorization, balances, expenses, recurrence. + - run: pnpm test:integration + + e2e: + runs-on: ubuntu-latest + timeout-minutes: 5 + 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 --prefer-offline + + - name: Resolve Playwright version + id: pw + run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT" + - name: Cache Playwright browsers + id: pw-cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.pw.outputs.version }} + - name: Install Chromium + run: | + if [ "${{ steps.pw-cache.outputs.cache-hit }}" = "true" ]; then + npx playwright install-deps chromium + else + npx playwright install --with-deps chromium + fi + + - name: Cache the production build + id: build-cache + uses: actions/cache@v4 + with: + path: dist + key: build-${{ runner.os }}-${{ hashFiles('src/**', 'worker/**', 'public/**', 'migrations/**', 'package.json', 'pnpm-lock.yaml', 'vite*.ts', 'next.config.js', 'wrangler.example.jsonc', 'tsconfig.json') }} + - name: Build + if: steps.build-cache.outputs.cache-hit != 'true' + run: | + cp wrangler.example.jsonc wrangler.jsonc + SKIP_ENV_VALIDATION=1 pnpm build + + - run: pnpm test:e2e + env: + CI: '1' + - name: Upload Playwright traces on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-traces + path: test-results + retention-days: 3 diff --git a/.gitignore b/.gitignore index 75a2de01f..005e75d8d 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,9 @@ dist/ # Personal Cloudflare deployment config (copy wrangler.example.jsonc to wrangler.jsonc) wrangler.jsonc + +# e2e artifacts +.e2e-state +.e2e-seed.sql +test-results +playwright-report diff --git a/README.md b/README.md index 6c95a7cce..43487ecb2 100644 --- a/README.md +++ b/README.md @@ -61,11 +61,26 @@ run `npx wrangler d1 migrations apply splitpro --local`, then `npx wrangler dev ## Known gaps -- No unit or end-to-end tests. The upstream Jest tests were mocked for Prisma, so they are excluded from - the TypeScript build and do not run. +- Test coverage is small and focused on the riskiest paths (see below). There are no unit or component tests. - Plaid / GoCardless bank sync and the Splitwise import are untested on Workers. - Only Google sign-in is wired up. There is no email or password login. - `drizzle-kit` and `wrangler d1 migrations` own the schema. `migrations/0001_balance_view_and_triggers.sql` is hand-written (the `BalanceView` view and a trigger). Back it up before regenerating migrations. +## Tests + +```sh +pnpm test:integration # Vitest in workerd with a real local D1 and the production migrations +pnpm build && pnpm test:e2e # Playwright against the built Worker (wrangler dev), 4 parallel workers +``` + +- `src/tests/integration`: authorization, balances and simplification, expense create/edit/delete + atomicity, settlements, recurring expenses (including idempotent retries), and regression tests + for D1 query bugs. +- `tests/e2e`: sign-in redirect, signed-in session, groups, expenses, settle up, a stranger being + blocked, and recurring expenses, all against the production bundle. Google sign-in cannot be + automated, so the tests seed users and signed `better-auth` session cookies in a throwaway D1. +- CI (`.github/workflows/ci.yml`) runs typecheck, integration, and e2e as three parallel jobs with + the pnpm store, Playwright browsers, and the build cached. The target is under 2 minutes. + Bug reports for this fork go to this repository's issues. Please do not report them upstream. diff --git a/jest.config.ts b/jest.config.ts deleted file mode 100644 index f45658bc4..000000000 --- a/jest.config.ts +++ /dev/null @@ -1,214 +0,0 @@ -/** - * For a detailed explanation regarding each configuration property, visit: - * https://jestjs.io/docs/configuration - */ - -import type { Config } from 'jest'; -import nextJest from 'next/jest.js'; - -// @ts-expect-error we are extending BigInt prototype for JSON serialization -// oxlint-disable-next-line no-extend-native -BigInt.prototype.toJSON = function toJSON() { - // Custom JSON serialization for BigInt to avoid errors in Jest - return this.toString(); -}; - -const createJestConfig = nextJest({ - // Provide the path to your Next.js app to load next.config.js and .env files in your test environment - dir: './', -}); - -const config: Config = { - // All imported modules in your tests should be mocked automatically - // automock: false, - - // Stop running tests after `n` failures - // bail: 0, - - // The directory where Jest should store its cached dependency information - // cacheDirectory: "C:\\Users\\Wiktor\\AppData\\Local\\Temp\\jest", - - // Automatically clear mock calls, instances, contexts and results before every test - // clearMocks: false, - - // Indicates whether the coverage information should be collected while executing the test - // collectCoverage: false, - - // An array of glob patterns indicating a set of files for which coverage information should be collected - // collectCoverageFrom: undefined, - - // The directory where Jest should output its coverage files - // coverageDirectory: undefined, - - // An array of regexp pattern strings used to skip coverage collection - // coveragePathIgnorePatterns: [ - // "\\\\node_modules\\\\" - // ], - - // Indicates which provider should be used to instrument code for coverage - coverageProvider: 'v8', - - // A list of reporter names that Jest uses when writing coverage reports - // coverageReporters: [ - // "json", - // "text", - // "lcov", - // "clover" - // ], - - // An object that configures minimum threshold enforcement for coverage results - // coverageThreshold: undefined, - - // A path to a custom dependency extractor - // dependencyExtractor: undefined, - - // Make calling deprecated APIs throw helpful error messages - // errorOnDeprecated: false, - - // The default configuration for fake timers - // fakeTimers: { - // "enableGlobally": false - // }, - - // Force coverage collection from ignored files using an array of glob patterns - // forceCoverageMatch: [], - - // A path to a module which exports an async function that is triggered once before all test suites - // globalSetup: undefined, - - // A path to a module which exports an async function that is triggered once after all test suites - // globalTeardown: undefined, - - // A set of global variables that need to be available in all test environments - // globals: {}, - - // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. - // maxWorkers: "50%", - - // An array of directory names to be searched recursively up from the requiring module's location - // moduleDirectories: [ - // "node_modules" - // ], - - // An array of file extensions your modules use - // moduleFileExtensions: [ - // "js", - // "mjs", - // "cjs", - // "jsx", - // "ts", - // "tsx", - // "json", - // "node" - // ], - - // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module - moduleNameMapper: { - '^~/(.*)$': '/src/$1', - }, - - // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader - // modulePathIgnorePatterns: [], - - // Activates notifications for test results - // notify: false, - - // An enum that specifies notification mode. Requires { notify: true } - // notifyMode: "failure-change", - - // A preset that is used as a base for Jest's configuration - // preset: undefined, - - // Run tests from one or more projects - // projects: undefined, - - // Use this configuration option to add custom reporters to Jest - // reporters: undefined, - - // Automatically reset mock state before every test - // resetMocks: false, - - // Reset the module registry before running each individual test - // resetModules: false, - - // A path to a custom resolver - // resolver: undefined, - - // Automatically restore mock state and implementation before every test - // restoreMocks: false, - - // The root directory that Jest should scan for tests and modules within - // rootDir: undefined, - - // A list of paths to directories that Jest should use to search for files in - // roots: [ - // "" - // ], - - // Allows you to use a custom runner instead of Jest's default test runner - // runner: "jest-runner", - - // The paths to modules that run some code to configure or set up the testing environment before each test - // setupFiles: [], - - // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], - - // The number of seconds after which a test is considered as slow and reported as such in the results. - // slowTestThreshold: 5, - - // A list of paths to snapshot serializer modules Jest should use for snapshot testing - // snapshotSerializers: [], - - // The test environment that will be used for testing - testEnvironment: 'jsdom', - - // Options that will be passed to the testEnvironment - // testEnvironmentOptions: {}, - - // Adds a location field to test results - // testLocationInResults: false, - - // The glob patterns Jest uses to detect test files - // testMatch: [ - // "**/__tests__/**/*.[jt]s?(x)", - // "**/?(*.)+(spec|test).[tj]s?(x)" - // ], - - // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped - // testPathIgnorePatterns: [ - // "\\\\node_modules\\\\" - // ], - - // The regexp pattern or array of patterns that Jest uses to detect test files - // testRegex: [], - - // This option allows the use of a custom results processor - // testResultsProcessor: undefined, - - // This option allows use of a custom test runner - // testRunner: "jest-circus/runner", - - // A map from regular expressions to paths to transformers - // transform: undefined, - - // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - // transformIgnorePatterns: [ - // "\\\\node_modules\\\\", - // "\\.pnp\\.[^\\\\]+$" - // ], - - // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them - // unmockedModulePathPatterns: undefined, - - // Indicates whether each individual test should be reported during the run - // verbose: undefined, - - // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode - // watchPathIgnorePatterns: [], - - // Whether to use watchman for file crawling - // watchman: true, -}; - -export default createJestConfig(config); diff --git a/package.json b/package.json index 21753adc9..d20807c68 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,9 @@ "db:verify-seed-hash": "tsx src/dummies/index.ts --strict", "dev": "vite dev", "lint": "oxlint --type-aware", - "test": "jest", - "test:watch": "jest --watch", + "test": "vitest run --project integration", + "test:e2e": "playwright test", + "test:integration": "vitest run --project integration", "prepare": "husky", "db:generate": "drizzle-kit generate", "db:migrate:local": "wrangler d1 migrations apply splitpro --local", @@ -68,11 +69,12 @@ }, "devDependencies": { "@cloudflare/vite-plugin": "1.63.1", + "@cloudflare/vitest-plugin": "1.4.0", "@cloudflare/workers-types": "5.20261011.1", "@faker-js/faker": "10.1.0", + "@playwright/test": "1.64.0", "@serwist/vite": "9.5.13", "@tailwindcss/postcss": "^4.1.10", - "@types/jest": "^29.5.14", "@types/node": "^22.15.21", "@types/react": "19.2.1", "@types/react-dom": "19.2.1", @@ -80,11 +82,12 @@ "@typescript/native-preview": "7.0.0-dev.20251016.1", "@vinext/cloudflare": "1.1.0", "@vitejs/plugin-react": "6.1.2", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", "drizzle-kit": "0.31.11", "husky": "^9.1.7", - "jest": "30.3.0", - "jest-environment-jsdom": "30.3.0", "jiti": "^2.5.1", + "jsdom": "30.1.2", "lint-staged": "^16.4.0", "oxlint": "1.57.0", "oxlint-tsgolint": "0.18.1", @@ -98,6 +101,7 @@ "tsx": "^4.7.1", "typescript": "~5.7.2", "vite": "8.3.4", + "vitest": "4.1.11", "wrangler": "4.149.0" }, "ct3aMetadata": { diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..293784d03 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from '@playwright/test'; + +const PORT = 3176; + +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + workers: 4, + retries: 0, + timeout: 30_000, + reporter: [['list']], + use: { baseURL: `http://127.0.0.1:${PORT}`, trace: 'retain-on-failure' }, + projects: [{ name: 'chromium', use: { browserName: 'chromium' } }], + webServer: { + // Runs the *built* Worker (dist/) in workerd with a throwaway local D1 under .e2e-state. + command: `node tests/e2e/prepare.mjs && npx wrangler dev -c dist/splitpro/wrangler.json --local --persist-to .e2e-state --ip 127.0.0.1 --port ${PORT} --var NEXTAUTH_URL:http://127.0.0.1:${PORT} --var NEXTAUTH_SECRET:e2e-secret-e2e-secret-e2e-secret-123456 --var GOOGLE_CLIENT_ID:e2e --var GOOGLE_CLIENT_SECRET:e2e`, + url: `http://127.0.0.1:${PORT}/api/healthz`, + timeout: 90_000, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 002a501c2..56d4a93e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,7 +45,7 @@ importers: version: 11.8.0(@trpc/server@11.8.0(typescript@5.7.3))(typescript@5.7.3) '@trpc/next': specifier: 11.2.0 - version: 11.2.0(@tanstack/react-query@5.90.12(react@19.2.1))(@trpc/client@11.8.0(@trpc/server@11.8.0(typescript@5.7.3))(typescript@5.7.3))(@trpc/react-query@11.8.0(@tanstack/react-query@5.90.12(react@19.2.1))(@trpc/client@11.8.0(@trpc/server@11.8.0(typescript@5.7.3))(typescript@5.7.3))(@trpc/server@11.8.0(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3))(@trpc/server@11.8.0(typescript@5.7.3))(next@15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3) + version: 11.2.0(@tanstack/react-query@5.90.12(react@19.2.1))(@trpc/client@11.8.0(@trpc/server@11.8.0(typescript@5.7.3))(typescript@5.7.3))(@trpc/react-query@11.8.0(@tanstack/react-query@5.90.12(react@19.2.1))(@trpc/client@11.8.0(@trpc/server@11.8.0(typescript@5.7.3))(typescript@5.7.3))(@trpc/server@11.8.0(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3))(@trpc/server@11.8.0(typescript@5.7.3))(next@15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3) '@trpc/react-query': specifier: ^11.2.0 version: 11.8.0(@tanstack/react-query@5.90.12(react@19.2.1))(@trpc/client@11.8.0(@trpc/server@11.8.0(typescript@5.7.3))(typescript@5.7.3))(@trpc/server@11.8.0(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3) @@ -54,7 +54,7 @@ importers: version: 11.8.0(typescript@5.7.3) better-auth: specifier: 1.7.7 - version: 1.7.7(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261011.1)(@types/better-sqlite3@9.6.0)(better-sqlite3@13.0.3)(kysely@0.29.6))(next@15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 1.7.7(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261011.1)(@types/better-sqlite3@9.6.0)(better-sqlite3@13.0.3)(kysely@0.29.6))(next@15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vitest@4.1.11(@types/node@22.19.3)(jsdom@30.1.2(@noble/hashes@2.4.0))(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))) boring-avatars: specifier: ^1.10.1 version: 1.11.2 @@ -102,7 +102,7 @@ importers: version: 5.1.6 next-i18next: specifier: ^15.4.3 - version: 15.4.3(i18next@25.10.10(typescript@5.7.3))(next@15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-i18next@15.7.4(i18next@25.10.10(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3))(react@19.2.1) + version: 15.4.3(i18next@25.10.10(typescript@5.7.3))(next@15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-i18next@15.7.4(i18next@25.10.10(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3))(react@19.2.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -147,7 +147,7 @@ importers: version: 1.1.2(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) vinext: specifier: 1.1.0 - version: 1.1.0(@types/node@22.19.3)(@vitejs/plugin-react@6.1.2(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))(next@15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 1.1.0(@types/node@22.19.3)(@vitejs/plugin-react@6.1.2(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))(next@15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) web-push: specifier: ^3.6.7 version: 3.6.7 @@ -161,21 +161,24 @@ importers: '@cloudflare/vite-plugin': specifier: 1.63.1 version: 1.63.1(@types/node@22.19.3)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(wrangler@4.149.0(@cloudflare/workers-types@5.20261011.1)(@types/node@22.19.3)) + '@cloudflare/vitest-plugin': + specifier: 1.4.0 + version: 1.4.0(@cloudflare/workers-types@5.20261011.1)(@types/node@22.19.3)(vitest@4.1.11(@types/node@22.19.3)(jsdom@30.1.2(@noble/hashes@2.4.0))(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))) '@cloudflare/workers-types': specifier: 5.20261011.1 version: 5.20261011.1 '@faker-js/faker': specifier: 10.1.0 version: 10.1.0 + '@playwright/test': + specifier: 1.64.0 + version: 1.64.0 '@serwist/vite': specifier: 9.5.13 version: 9.5.13(browserslist@4.28.1)(typescript@5.7.3)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) '@tailwindcss/postcss': specifier: ^4.1.10 version: 4.1.18 - '@types/jest': - specifier: ^29.5.14 - version: 29.5.14 '@types/node': specifier: ^22.15.21 version: 22.19.3 @@ -193,25 +196,28 @@ importers: version: 7.0.0-dev.20251016.1 '@vinext/cloudflare': specifier: 1.1.0 - version: 1.1.0(@cloudflare/vite-plugin@1.63.1(@types/node@22.19.3)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(wrangler@4.149.0(@cloudflare/workers-types@5.20261011.1)(@types/node@22.19.3)))(vinext@1.1.0(@types/node@22.19.3)(@vitejs/plugin-react@6.1.2(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))(next@15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 1.1.0(@cloudflare/vite-plugin@1.63.1(@types/node@22.19.3)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(wrangler@4.149.0(@cloudflare/workers-types@5.20261011.1)(@types/node@22.19.3)))(vinext@1.1.0(@types/node@22.19.3)(@vitejs/plugin-react@6.1.2(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))(next@15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))) '@vitejs/plugin-react': specifier: 6.1.2 version: 6.1.2(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/runner': + specifier: 4.1.11 + version: 4.1.11 + '@vitest/snapshot': + specifier: 4.1.11 + version: 4.1.11 drizzle-kit: specifier: 0.31.11 version: 0.31.11 husky: specifier: ^9.1.7 version: 9.1.7 - jest: - specifier: 30.3.0 - version: 30.3.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.7.3)) - jest-environment-jsdom: - specifier: 30.3.0 - version: 30.3.0 jiti: specifier: ^2.5.1 version: 2.6.1 + jsdom: + specifier: 30.1.2 + version: 30.1.2(@noble/hashes@2.4.0) lint-staged: specifier: ^16.4.0 version: 16.4.0 @@ -251,6 +257,9 @@ importers: vite: specifier: 8.3.4 version: 8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vitest: + specifier: 4.1.11 + version: 4.1.11(@types/node@22.19.3)(jsdom@30.1.2(@noble/hashes@2.4.0))(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) wrangler: specifier: 4.149.0 version: 4.149.0(@cloudflare/workers-types@5.20261011.1)(@types/node@22.19.3) @@ -261,162 +270,13 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - - '@asamuzakjp/css-color@3.2.0': - resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} - - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.27.2': - resolution: {integrity: sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.28.3': - resolution: {integrity: sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.28.3': - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.27.2': - resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.27.1': - resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.3': - resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-plugin-utils@7.27.1': - resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.28.3': - resolution: {integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.28.3': - resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-bigint@7.8.3': - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-attributes@7.27.1': - resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-meta@7.10.4': - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-jsx@7.27.1': - resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@asamuzakjp/css-color@7.1.5': + resolution: {integrity: sha512-jCpAvj8apa7SKHZYogk0rbXB3Kb0hLFrpkWF+Ap+vBA5rirItnpv9NKyAyEuDuFizvl0qwnj6JWR70RGJ0rOlw==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.27.1': - resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@asamuzakjp/dom-selector@9.2.4': + resolution: {integrity: sha512-YnVzxDxLaqL0t7Q4wfcgHZjg55Wm6y5EaO7Bhu3q9J7wq/8iw3PhsnWiOQhYekx4VexnbkgWpTT6n+QIV7kczw==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} '@babel/runtime@7.27.6': resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} @@ -430,21 +290,6 @@ packages: resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.28.3': - resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.28.2': - resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} - engines: {node: '>=6.9.0'} - - '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@better-auth/core@1.7.7': resolution: {integrity: sha512-srgFzHEjB2WWlokFz1i4BfG41sNhsA105Pukgqu6RrEh5A+LSkzf1mCHydVipkjt9l5qswtBMi7IL6ouHbZL4Q==} peerDependencies: @@ -524,6 +369,10 @@ packages: '@better-fetch/fetch@1.3.2': resolution: {integrity: sha512-Gs7n99b5tqUC6cQAPbV0uED3IraHB6xQbHLQ/C3l7ZFafHScOx9pQ+DYmP5blbLFShVWLqxNUlI9wi4xU/X+ow==} + '@bramus/specificity@2.4.3': + resolution: {integrity: sha512-0GEz8qJWokkUVYQYRukmkSHIFNoBH8QTreeHrAFbvz1XFYc3Y+puhMs8PQ5x8GNugiC2l5jvDBXKLbBIByeSBw==} + hasBin: true + '@cloudflare/kv-asset-handler@0.5.0': resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} engines: {node: '>=22.0.0'} @@ -544,6 +393,11 @@ packages: vite: ^6.1.0 || ^7.0.0 || ^8.0.0 wrangler: ^4.149.0 + '@cloudflare/vitest-plugin@1.4.0': + resolution: {integrity: sha512-SgIhyhpVH+2MVSJxidG8gosqrpzF25IwKevQhPykwulTgAFe4s0Nkj71pNzqCkwV2+A4NKkHJP2ReyyRsoIxsg==} + peerDependencies: + vitest: ^4.1.0 || ^5.0.0 + '@cloudflare/workerd-darwin-64@1.20261006.1': resolution: {integrity: sha512-lCX6avJO2It9AyDEYitjmOtJRVp/xmaW0TziyoLAnfxlzozyyRbSFfIIMMCiDx/sTP1++E4VLbV+7BHSD33f0Q==} engines: {node: '>=16'} @@ -585,33 +439,41 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@csstools/color-helpers@5.1.0': - resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} - engines: {node: '>=18'} + '@csstools/color-helpers@6.1.2': + resolution: {integrity: sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==} + engines: {node: '>=20.19.0'} - '@csstools/css-calc@2.1.4': - resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} - engines: {node: '>=18'} + '@csstools/css-calc@3.4.3': + resolution: {integrity: sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==} + engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms': ^4.0.2 + '@csstools/css-tokenizer': ^4.0.2 - '@csstools/css-color-parser@3.1.0': - resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} - engines: {node: '>=18'} + '@csstools/css-color-parser@4.2.6': + resolution: {integrity: sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==} + engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms': ^4.0.2 + '@csstools/css-tokenizer': ^4.0.2 - '@csstools/css-parser-algorithms@3.0.5': - resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} - engines: {node: '>=18'} + '@csstools/css-parser-algorithms@4.0.2': + resolution: {integrity: sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==} + engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-tokenizer': ^4.0.2 - '@csstools/css-tokenizer@3.0.4': - resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} - engines: {node: '>=18'} + '@csstools/css-syntax-patches-for-csstree@1.1.15': + resolution: {integrity: sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.2': + resolution: {integrity: sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==} + engines: {node: '>=20.19.0'} '@date-fns/tz@1.4.1': resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} @@ -619,18 +481,9 @@ packages: '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} - '@emnapi/core@1.4.5': - resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} - '@emnapi/runtime@1.11.3': resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} - '@emnapi/runtime@1.7.1': - resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} - - '@emnapi/wasi-threads@1.0.4': - resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} - '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} deprecated: 'Merged into tsx: https://tsx.hirok.io' @@ -1239,6 +1092,15 @@ packages: cpu: [x64] os: [win32] + '@exodus/bytes@1.16.0': + resolution: {integrity: sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@faker-js/faker@10.1.0': resolution: {integrity: sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==} engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} @@ -1273,32 +1135,16 @@ packages: peerDependencies: react: ^16.13 || ^17 || ^18 || ^19 - '@img/colour@1.0.0': - resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} - engines: {node: '>=18'} - '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - '@img/sharp-darwin-arm64@0.35.5': resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - '@img/sharp-darwin-x64@0.35.5': resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} engines: {node: '>=20.9.0'} @@ -1310,207 +1156,104 @@ packages: engines: {node: '>=20.9.0'} os: [freebsd] - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} - cpu: [arm64] - os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.3.4': resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} - cpu: [x64] - os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.4': resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} - cpu: [arm64] - os: [linux] - '@img/sharp-libvips-linux-arm64@1.3.4': resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} - cpu: [arm] - os: [linux] - '@img/sharp-libvips-linux-arm@1.3.4': resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} - cpu: [ppc64] - os: [linux] - '@img/sharp-libvips-linux-ppc64@1.3.4': resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} - cpu: [riscv64] - os: [linux] - '@img/sharp-libvips-linux-riscv64@1.3.4': resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} cpu: [riscv64] os: [linux] - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} - cpu: [s390x] - os: [linux] - '@img/sharp-libvips-linux-s390x@1.3.4': resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} - cpu: [x64] - os: [linux] - '@img/sharp-libvips-linux-x64@1.3.4': resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} - cpu: [arm64] - os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.3.4': resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} - cpu: [x64] - os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.3.4': resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - '@img/sharp-linux-arm64@0.35.5': resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - '@img/sharp-linux-arm@0.35.5': resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] - os: [linux] - '@img/sharp-linux-ppc64@0.35.5': resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [riscv64] - os: [linux] - '@img/sharp-linux-riscv64@0.35.5': resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - '@img/sharp-linux-s390x@0.35.5': resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - '@img/sharp-linux-x64@0.35.5': resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - '@img/sharp-linuxmusl-arm64@0.35.5': resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - '@img/sharp-linuxmusl-x64@0.35.5': resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - '@img/sharp-wasm32@0.35.5': resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} engines: {node: '>=20.9.0'} @@ -1520,158 +1263,24 @@ packages: engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] - '@img/sharp-win32-arm64@0.35.5': resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - '@img/sharp-win32-ia32@0.35.5': resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - '@img/sharp-win32-x64@0.35.5': resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@istanbuljs/load-nyc-config@1.1.0': - resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} - engines: {node: '>=8'} - - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - - '@jest/console@30.3.0': - resolution: {integrity: sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/core@30.3.0': - resolution: {integrity: sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - '@jest/diff-sequences@30.3.0': - resolution: {integrity: sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/environment-jsdom-abstract@30.3.0': - resolution: {integrity: sha512-0hNFs5N6We3DMCwobzI0ydhkY10sT1tZSC0AAiy+0g2Dt/qEWgrcV5BrMxPczhe41cxW4qm6X+jqZaUdpZIajA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - peerDependencies: - canvas: ^3.0.0 - jsdom: '*' - peerDependenciesMeta: - canvas: - optional: true - - '@jest/environment@30.3.0': - resolution: {integrity: sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/expect-utils@29.7.0': - resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/expect-utils@30.3.0': - resolution: {integrity: sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/expect@30.3.0': - resolution: {integrity: sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/fake-timers@30.3.0': - resolution: {integrity: sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/get-type@30.1.0': - resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/globals@30.3.0': - resolution: {integrity: sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/pattern@30.0.1': - resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/reporters@30.3.0': - resolution: {integrity: sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/schemas@30.0.5': - resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/snapshot-utils@30.3.0': - resolution: {integrity: sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/source-map@30.0.1': - resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/test-result@30.3.0': - resolution: {integrity: sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/test-sequencer@30.3.0': - resolution: {integrity: sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/transform@30.3.0': - resolution: {integrity: sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/types@29.6.3': - resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/types@30.3.0': - resolution: {integrity: sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1694,9 +1303,6 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@napi-rs/wasm-runtime@0.2.12': - resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@next/env@15.5.27': resolution: {integrity: sha512-WC5hvVqiuFOcRyz6gKPeJkiVvSMf3scNeAV49CdWtVqxmUlzh8UKR6YHt9yiNkh8LngtzqSlVz07/Ma3ZAzchA==} @@ -1919,13 +1525,10 @@ packages: cpu: [x64] os: [win32] - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - - '@pkgr/core@0.2.9': - resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.64.0': + resolution: {integrity: sha512-oTN9FNYuN6bqQMwiho6jJKLCG2/QOYCixGXuCjRmzOq23eODeukuY2XiAVoVpAECbuezMt1MHQjAiQxK8F+Y+g==} + engines: {node: '>=20'} + hasBin: true '@poppinss/colors@4.1.6': resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} @@ -2809,22 +2412,10 @@ packages: engines: {node: '>= 8.0.0'} hasBin: true - '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - - '@sinclair/typebox@0.34.40': - resolution: {integrity: sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw==} - '@sindresorhus/is@7.2.0': resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} - '@sinonjs/commons@3.0.1': - resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} - - '@sinonjs/fake-timers@15.2.2': - resolution: {integrity: sha512-KK6HfcJHaxmQNzHSsCrRzp1igwAWUXEtD64EScEUsaHw3ViNHgpFR/ACgHpesDR/YDbu6IZMzNbQkTDMlnZHGw==} - '@speed-highlight/core@1.2.24': resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==} @@ -3014,41 +2605,20 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - '@tybys/wasm-util@0.10.0': - resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.20.7': - resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} - '@types/better-sqlite3@9.6.0': resolution: {integrity: sha512-ZEEwBSgMu7GYJOynoagg5X9JbxfL6dTJDsgViJIqh67jV44kyOr9RXfmFjLK5rzC4MWssP06t9hu/JwGDnUbCg==} - '@types/hoist-non-react-statics@3.3.6': - resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==} - - '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} - - '@types/istanbul-lib-report@3.0.3': - resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/istanbul-reports@3.0.4': - resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@types/jest@29.5.14': - resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/jsdom@21.1.7': - resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} + '@types/hoist-non-react-statics@3.3.6': + resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==} '@types/node@22.19.3': resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} @@ -3061,21 +2631,9 @@ packages: '@types/react@19.2.1': resolution: {integrity: sha512-1U5NQWh/GylZQ50ZMnnPjkYHEaGhg6t5i/KI0LDDh3t4E3h3T3vzm+GLY2BRzMfIjSBwzm6tginoZl5z0O/qsA==} - '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - - '@types/tough-cookie@4.0.5': - resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} - '@types/web-push@3.6.4': resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==} - '@types/yargs-parser@21.0.3': - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - - '@types/yargs@17.0.33': - resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251016.1': resolution: {integrity: sha512-TBSMWAhEaHGMwDRYxY5Lpjgr0JMdtdYEr/ha1pdvAP7g+D23EnKNsIpf2bv2FoBkWb5gsh33tT6GcZV0N2bICg==} cpu: [arm64] @@ -3115,9 +2673,6 @@ packages: resolution: {integrity: sha512-OMnnupqcqdcbf4lryAKyIDWNJHc1dP13OcKmV5aPUY2dJIYBD0ud4xgOkp/zdOoLBCpdRMIMjzziI6PYATMWdw==} hasBin: true - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@unpic/core@1.0.3': resolution: {integrity: sha512-aum9YNVUGso7MjGLD0Rp/08kywCGLqZ03/q6VQBFFakDBOXWEc8D4kPGcZ8v5wEnGRex3lE+++bOuucBp3KJ/w==} @@ -3131,104 +2686,9 @@ packages: next: optional: true - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} - cpu: [arm] - os: [android] - - '@unrs/resolver-binding-android-arm64@1.11.1': - resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} - cpu: [arm64] - os: [android] - - '@unrs/resolver-binding-darwin-arm64@1.11.1': - resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} - cpu: [arm64] - os: [darwin] - - '@unrs/resolver-binding-darwin-x64@1.11.1': - resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} - cpu: [x64] - os: [darwin] - - '@unrs/resolver-binding-freebsd-x64@1.11.1': - resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} - cpu: [x64] - os: [freebsd] - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} - cpu: [arm64] - os: [linux] - - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} - cpu: [arm64] - os: [linux] - - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} - cpu: [ppc64] - os: [linux] - - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} - cpu: [riscv64] - os: [linux] - - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} - cpu: [riscv64] - os: [linux] - - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} - cpu: [s390x] - os: [linux] - - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} - cpu: [x64] - os: [linux] - - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} - cpu: [x64] - os: [linux] - - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} - cpu: [arm64] - os: [win32] - - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} - cpu: [ia32] - os: [win32] - - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} - cpu: [x64] - os: [win32] - - '@vercel/og@1.0.3': - resolution: {integrity: sha512-/iYJ34PDu4lty1TXdL86qHEw8viveHTllG1PWLuJX8tSFfqKL7kyVJeUORJmQ250bHB0h+alcalXUlza3c/NSA==} - engines: {node: '>=22'} + '@vercel/og@1.0.3': + resolution: {integrity: sha512-/iYJ34PDu4lty1TXdL86qHEw8viveHTllG1PWLuJX8tSFfqKL7kyVJeUORJmQ250bHB0h+alcalXUlza3c/NSA==} + engines: {node: '>=22'} '@vinext/cloudflare@1.1.0': resolution: {integrity: sha512-AQVYlrGuu85HckaENIi87yN84eOFvXI29aBA0yp16Sq7iol2BKlNJTv09RSix0R8CxyUgQmJ5f3aHZifahGYHQ==} @@ -3264,6 +2724,35 @@ packages: oxc-transform-react: optional: true + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + acorn-walk@8.3.4: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} @@ -3277,44 +2766,21 @@ packages: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} - ansi-escapes@7.2.0: resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} engines: {node: '>=18'} - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - ansi-regex@6.2.2: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - aria-hidden@1.2.6: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} @@ -3322,37 +2788,16 @@ packages: asn1.js@5.4.1: resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} axios@1.16.0: resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==} - babel-jest@30.3.0: - resolution: {integrity: sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - peerDependencies: - '@babel/core': ^7.11.0 || ^8.0.0-0 - - babel-plugin-istanbul@7.0.1: - resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} - engines: {node: '>=12'} - - babel-plugin-jest-hoist@30.3.0: - resolution: {integrity: sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - babel-preset-current-node-syntax@1.2.0: - resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} - peerDependencies: - '@babel/core': ^7.0.0 || ^8.0.0-0 - - babel-preset-jest@30.3.0: - resolution: {integrity: sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - peerDependencies: - '@babel/core': ^7.11.0 || ^8.0.0-beta.1 - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -3435,6 +2880,9 @@ packages: resolution: {integrity: sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==} engines: {node: '>=22'} + bidi-js@1.1.0: + resolution: {integrity: sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==} + blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} @@ -3459,9 +2907,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - bser@2.1.1: - resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -3472,42 +2917,18 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} caniuse-lite@1.0.30001760: resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} - - ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} - - ci-info@4.3.0: - resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} - engines: {node: '>=8'} + chai@6.3.0: + resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} + engines: {node: '>=18'} - cjs-module-lexer@2.1.0: - resolution: {integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==} + cjs-module-lexer@1.2.3: + resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -3523,10 +2944,6 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -3537,17 +2954,6 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc - co@4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - - collect-v8-coverage@1.0.2: - resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -3594,10 +3000,6 @@ packages: cross-fetch@4.1.0: resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - css-background-parser@0.1.0: resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} @@ -3615,16 +3017,16 @@ packages: css-to-react-native@3.2.0: resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} - cssstyle@4.6.0: - resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} - engines: {node: '>=18'} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - data-urls@5.0.0: - resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} - engines: {node: '>=18'} + data-urls@8.0.0: + resolution: {integrity: sha512-JQUCCK+/CAGbGGx618PlFZMXH81Jtj7t73CHv9dd/zVWvxgGT2zLFRjnKb+Ng46GIwcy+f7f88qd4duqllK+WQ==} + engines: {node: ^22.14.0 || >=24.0.0} date-fns-jalali@4.1.0-0: resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} @@ -3644,20 +3046,8 @@ packages: supports-color: optional: true - decimal.js@10.5.0: - resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} - - dedent@1.6.0: - resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==} - peerDependencies: - babel-plugin-macros: ^3.1.0 - peerDependenciesMeta: - babel-plugin-macros: - optional: true - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} @@ -3670,17 +3060,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} - detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - diff@4.0.4: resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} engines: {node: '>=0.3.1'} @@ -3792,19 +3174,12 @@ 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==} electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} - emittery@0.13.1: - resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} - engines: {node: '>=12'} - emoji-regex-xs@2.0.1: resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==} engines: {node: '>=10.0.0'} @@ -3812,27 +3187,18 @@ packages: emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - enhanced-resolve@5.18.4: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} - entities@6.0.0: - resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} - engines: {node: '>=0.12'} + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} + engines: {node: '>=20.19.0'} environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} @@ -3847,6 +3213,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -3882,47 +3251,23 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - - exit-x@0.2.2: - resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} - engines: {node: '>= 0.8.0'} - - expect@29.7.0: - resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - expect@30.3.0: - resolution: {integrity: sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - fastq@1.20.3: resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} - fb-watchman@2.0.2: - resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -3939,10 +3284,6 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - follow-redirects@1.16.0: resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} @@ -3952,17 +3293,10 @@ packages: debug: optional: true - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3971,14 +3305,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.4.0: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} @@ -3991,18 +3317,10 @@ packages: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} - get-package-type@0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} - get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} @@ -4010,18 +3328,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - hasBin: true - glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -4032,10 +3342,6 @@ packages: harfbuzzjs@0.10.0: resolution: {integrity: sha512-SN8LVwCOzvTq3OPNd0+EAghgXugItz56wst567D1vs7LnnKGWprhi3EG58aVOBwhN8HEbQxL4I9NDWkcXUutRw==} - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -4055,20 +3361,13 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - html-encoding-sniffer@4.0.0: - resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} - engines: {node: '>=18'} - - html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-encoding-sniffer@7.0.0: + resolution: {integrity: sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==} + engines: {node: ^22.13.0 || >=24.0.0} html-parse-stringify@3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} - http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} - http_ece@1.2.0: resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==} engines: {node: '>=16'} @@ -4077,10 +3376,6 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -4103,26 +3398,9 @@ packages: typescript: optional: true - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - idb@8.0.3: resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==} - import-local@3.2.0: - resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} - engines: {node: '>=8'} - hasBin: true - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -4136,25 +3414,14 @@ packages: resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==} engines: {node: '>= 10'} - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - is-fullwidth-code-point@5.1.0: resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} engines: {node: '>=18'} - is-generator-fn@2.1.0: - resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} - engines: {node: '>=6'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -4166,197 +3433,10 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - is-what@5.5.0: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - - istanbul-lib-instrument@6.0.3: - resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} - engines: {node: '>=10'} - - istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} - - istanbul-lib-source-maps@5.0.6: - resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} - engines: {node: '>=10'} - - istanbul-reports@3.1.7: - resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} - engines: {node: '>=8'} - - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - - jest-changed-files@30.3.0: - resolution: {integrity: sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-circus@30.3.0: - resolution: {integrity: sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-cli@30.3.0: - resolution: {integrity: sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - jest-config@30.3.0: - resolution: {integrity: sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - peerDependencies: - '@types/node': '*' - esbuild-register: '>=3.4.0' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - esbuild-register: - optional: true - ts-node: - optional: true - - jest-diff@29.7.0: - resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-diff@30.3.0: - resolution: {integrity: sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-docblock@30.2.0: - resolution: {integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-each@30.3.0: - resolution: {integrity: sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-environment-jsdom@30.3.0: - resolution: {integrity: sha512-RLEOJy6ip1lpw0yqJ8tB3i88FC7VBz7i00Zvl2qF71IdxjS98gC9/0SPWYIBVXHm5hgCYK0PAlSlnHGGy9RoMg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - peerDependencies: - canvas: ^3.0.0 - peerDependenciesMeta: - canvas: - optional: true - - jest-environment-node@30.3.0: - resolution: {integrity: sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-get-type@29.6.3: - resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-haste-map@30.3.0: - resolution: {integrity: sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-leak-detector@30.3.0: - resolution: {integrity: sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-matcher-utils@29.7.0: - resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-matcher-utils@30.3.0: - resolution: {integrity: sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-message-util@29.7.0: - resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-message-util@30.3.0: - resolution: {integrity: sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-mock@30.3.0: - resolution: {integrity: sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-pnp-resolver@1.2.3: - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true - - jest-regex-util@30.0.1: - resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-resolve-dependencies@30.3.0: - resolution: {integrity: sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-resolve@30.3.0: - resolution: {integrity: sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-runner@30.3.0: - resolution: {integrity: sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-runtime@30.3.0: - resolution: {integrity: sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-snapshot@30.3.0: - resolution: {integrity: sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-util@29.7.0: - resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-util@30.3.0: - resolution: {integrity: sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-validate@30.3.0: - resolution: {integrity: sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-watcher@30.3.0: - resolution: {integrity: sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-worker@30.3.0: - resolution: {integrity: sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest@30.3.0: - resolution: {integrity: sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -4367,32 +3447,15 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.2: - resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} - hasBin: true - - jsdom@26.1.0: - resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} - engines: {node: '>=18'} + jsdom@30.1.2: + resolution: {integrity: sha512-0FFE/jE1rppmVfUrJUgxqXjcwolZYIGtAgj1pTussMhNZxIxi7W/PvfWaMNroGi2B36X1IKHbexpZ9DhkoOPiQ==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} peerDependencies: - canvas: ^3.0.0 + canvas: ^3.2.3 peerDependenciesMeta: canvas: optional: true - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} @@ -4410,10 +3473,6 @@ packages: resolution: {integrity: sha512-hHaB8C/rfzDDtr/t8YZwxAuPJTT0zHyaPoVzcXwDYhYNAgH/4sIfVhi/XLLIY+bL/FqaIJnjATDbi8ObSELmxg==} engines: {node: '>=22.0.0'} - leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -4557,9 +3616,6 @@ packages: linebreak@1.1.0: resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - lint-staged@16.4.0: resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} engines: {node: '>=20.17'} @@ -4569,10 +3625,6 @@ packages: resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} engines: {node: '>=20.0.0'} - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -4581,16 +3633,10 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.5.3: resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} engines: {node: 20 || >=22} - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@0.544.0: resolution: {integrity: sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==} peerDependencies: @@ -4603,22 +3649,15 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} - make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} @@ -4636,10 +3675,6 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -4658,10 +3693,6 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -4688,14 +3719,6 @@ packages: resolution: {integrity: sha512-FixtE239Gl6Gz0ZGCK0WzvNwObNCIwVT+L7zQcf1QgcA6AzBxblzqe9OT88qiucNkhsjSOqiogSaF5jVbJiqSA==} engines: {node: ^20.0.0 || >=22.0.0} - napi-postinstall@0.3.3: - resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next-i18next@15.4.3: resolution: {integrity: sha512-ZRmiz72o1Jvh2ZghCUQX1Ua5F/f2W1/Ila/L1ZeKVuSWiH7J4zfUedfDxNBEhj9lajREC7aoJuPXMFtKi2bdIg==} engines: {node: '>=14'} @@ -4745,39 +3768,22 @@ packages: encoding: optional: true - node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} nordigen-node@1.4.1: resolution: {integrity: sha512-m7U9scf9GEkLEwy03l+cxcfcDGAiRMtD1sWrEH/mpYrZEC1hDZ2wRg+eMndzDOnADZaUoPvcimushUUA4+csvg==} - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - normalize-wheel@1.0.1: resolution: {integrity: sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==} - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - - nwsapi@2.2.20: - resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} @@ -4797,53 +3803,14 @@ packages: oxlint-tsgolint: optional: true - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} parse-css-color@0.2.1: resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - - parse5@7.3.0: - resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} path-scurry@2.0.2: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} @@ -4866,18 +3833,20 @@ packages: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - plaid@38.1.0: resolution: {integrity: sha512-5CxHxiTEJUEohvxOhfNclWIrIKJYN6pODBoF7X6Iwr9nYYUsk2XsYzOMUMBybAFRTcanU2C2F5STPmsNxgT0PQ==} engines: {node: '>=10.0.0'} + playwright-core@1.64.0: + resolution: {integrity: sha512-T9r+MZkTECl2+oUcZ26YgZzTrYidTsIhYYkJix+6iDOymOb1FB2RLLSOxRGnZGv5DStt+aduIVbt+E8WygXYpg==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.64.0: + resolution: {integrity: sha512-kVzTnYFYEyQ9RGzBjNCd2+yEqWmVbxl3AHPynv3qQ6JTjv4a+kwRcnqQwIpuP3NnrnIrE/Q6wWAIAvMfIE2/Yg==} + engines: {node: '>=20'} + hasBin: true + postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -4963,14 +3932,6 @@ packages: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - pretty-format@30.3.0: - resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -4982,9 +3943,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - pure-rand@7.0.1: - resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -5043,9 +4001,6 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-plaid-link@4.1.1: resolution: {integrity: sha512-xzAYWQIT/gk+u6lwFAMEZ20f9+AUsCwVyfm64/iudMsyuWANta4wm3Jb7N+APSwuKIR9VUlTkYDhPjLamIGcPA==} peerDependencies: @@ -5096,18 +4051,10 @@ packages: resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==} engines: {node: '>=0.10.0'} - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - resolve-cwd@3.0.0: - resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} - engines: {node: '>=8'} - - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -5130,9 +4077,6 @@ packages: rou3@0.9.2: resolution: {integrity: sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==} - rrweb-cssom@0.8.0: - resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -5153,15 +4097,6 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} - engines: {node: '>=10'} - hasBin: true - semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -5186,10 +4121,6 @@ packages: set-cookie-parser@3.1.3: resolution: {integrity: sha512-xletSdAI5efyVJjsatBbzDkXl3M/Zmad3xfnQe7ZXuBB0yNwW7C2WFsNGjdRNfZunmNHQuX79xs/ACMTLCdcew==} - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - sharp@0.35.5: resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} engines: {node: '>=20.9.0'} @@ -5199,25 +4130,13 @@ packages: '@types/node': optional: true - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - slice-ansi@7.1.2: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} @@ -5236,9 +4155,6 @@ packages: resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} - source-map-support@0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} - source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -5250,29 +4166,16 @@ packages: resolution: {integrity: sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==} engines: {node: '>= 12'} - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} + std-env@4.3.0: + resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} - string-length@4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} - - string-width@4.2.3: - 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'} @@ -5284,26 +4187,10 @@ packages: string.prototype.codepointat@0.2.1: resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - strip-ansi@7.1.2: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} - strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} - - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -5325,21 +4212,6 @@ packages: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - - synckit@0.11.11: - resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} - engines: {node: ^14.18.0 || >=16.0.0} - tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} @@ -5354,13 +4226,12 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} - tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@1.0.4: resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} @@ -5369,30 +4240,31 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - tldts-core@6.1.86: - resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + tinyrainbow@3.2.0: + resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==} + engines: {node: '>=14.0.0'} - tldts@6.1.86: - resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} - hasBin: true + tldts-core@7.4.18: + resolution: {integrity: sha512-KI2PKvCmvQI/xD6ypPqkU0i7oxNXR/O58RLRaHR4svsgVUQ2JE/XC8BtMbG6PSehD27I8sZJRcC1ASnCy6lp3g==} - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + tldts@7.4.18: + resolution: {integrity: sha512-somxA8xKAUzlAxl0TX/jMSYIbRNFIVGnI96pVGJPeMc5BsHmfVXn6mOJT6U++ELnl04eGfzpfcEuZe8WiASkXg==} + hasBin: true to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - tough-cookie@5.1.2: - resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} engines: {node: '>=16'} tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@5.1.1: - resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} - engines: {node: '>=18'} + tr46@7.0.0: + resolution: {integrity: sha512-BcDSDW+SIGWVOns1Zm6Yg91q9AEj7eaID0QDY9oIqJD1A8dscNwMcl9Sms2DeuWCNov1X0/oXanHGYQBSLwZsg==} + engines: {node: ^22.14.0 || >=24.0.0} ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} @@ -5416,14 +4288,6 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - type-fest@5.10.0: resolution: {integrity: sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==} engines: {node: '>=20'} @@ -5440,6 +4304,10 @@ packages: resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==} engines: {node: '>=20.18.1'} + undici@8.11.2: + resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==} + engines: {node: '>=22.19.0'} + unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -5449,9 +4317,6 @@ packages: unpic@4.2.2: resolution: {integrity: sha512-z6T2ScMgRV2y2H8MwwhY5xHZWXhUx/YxtOCGJwfURSl7ypVy4HpLIMWoIZKnnxQa/RKzM0kg8hUh0paIrpLfvw==} - unrs-resolver@1.11.1: - resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -5489,10 +4354,6 @@ packages: v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - v8-to-istanbul@9.3.0: - resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} - engines: {node: '>=10.12.0'} - vaul@1.1.2: resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} peerDependencies: @@ -5568,16 +4429,54 @@ packages: yaml: optional: true + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} - w3c-xmlserializer@5.0.0: - resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} - engines: {node: '>=18'} - - walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + w3c-xmlserializer@6.0.0: + resolution: {integrity: sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} web-push@3.6.7: resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==} @@ -5590,29 +4489,24 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} - - whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} - deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} - whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} - whatwg-url@14.2.0: - resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} - engines: {node: '>=18'} + whatwg-url@17.2.0: + resolution: {integrity: sha512-IhLIHNcBrzIixC5XXsbr4DxdjajyA5vQ5RwGZMtrUp+GbtE5Nrsge9QKau7jXrC0aTXLYGV0xiW7Tb4YS2aomw==} + engines: {node: ^22.14.0 || >=24.0.0} whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} hasBin: true workerd@1.20261006.1: @@ -5630,37 +4524,10 @@ packages: '@cloudflare/workers-types': optional: true - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - write-file-atomic@5.0.1: - resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - ws@8.18.2: - resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} @@ -5680,34 +4547,15 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yaml@2.8.3: resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} hasBin: true - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} @@ -5742,182 +4590,20 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@asamuzakjp/css-color@3.2.0': - dependencies: - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 - lru-cache: 10.4.3 - - '@babel/code-frame@7.27.1': - dependencies: - '@babel/helper-validator-identifier': 7.27.1 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.27.2': {} - - '@babel/core@7.28.3': - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) - '@babel/helpers': 7.28.3 - '@babel/parser': 7.28.3 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 - convert-source-map: 2.0.0 - debug: 4.4.1 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.28.3': - dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.27.2': - dependencies: - '@babel/compat-data': 7.27.2 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.28.0': {} - - '@babel/helper-module-imports@7.27.1': - dependencies: - '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.3 - transitivePeerDependencies: - - supports-color - - '@babel/helper-plugin-utils@7.27.1': {} - - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.27.1': {} - - '@babel/helper-validator-option@7.27.1': {} - - '@babel/helpers@7.28.3': - dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.2 - - '@babel/parser@7.28.3': - dependencies: - '@babel/types': 7.28.2 - - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.3)': + '@asamuzakjp/css-color@7.1.5': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-color-parser': 4.2.6(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + lru-cache: 11.5.3 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.3)': + '@asamuzakjp/dom-selector@9.2.4': dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 + bidi-js: 1.1.0 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.3 '@babel/runtime@7.27.6': {} @@ -5925,31 +4611,6 @@ snapshots: '@babel/runtime@7.29.2': {} - '@babel/template@7.27.2': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 - - '@babel/traverse@7.28.3': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.3 - '@babel/template': 7.27.2 - '@babel/types': 7.28.2 - debug: 4.4.1 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.28.2': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - - '@bcoe/v8-coverage@0.2.3': {} - '@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@3.25.76))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5)': dependencies: '@better-auth/utils': 0.4.2 @@ -6007,6 +4668,10 @@ snapshots: '@better-fetch/fetch@1.3.2': {} + '@bramus/specificity@2.4.3': + dependencies: + css-tree: 3.2.1 + '@cloudflare/kv-asset-handler@0.5.0': {} '@cloudflare/unenv-preset@2.16.2(unenv@2.0.0-rc.24)(workerd@1.20261006.1)': @@ -6029,6 +4694,20 @@ snapshots: - bufferutil - utf-8-validate + '@cloudflare/vitest-plugin@1.4.0(@cloudflare/workers-types@5.20261011.1)(@types/node@22.19.3)(vitest@4.1.11(@types/node@22.19.3)(jsdom@30.1.2(@noble/hashes@2.4.0))(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + cjs-module-lexer: 1.2.3 + esbuild: 0.28.2 + miniflare: 5.20261006.1-alpha(@types/node@22.19.3) + vitest: 4.1.11(@types/node@22.19.3)(jsdom@30.1.2(@noble/hashes@2.4.0))(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + wrangler: 4.149.0(@cloudflare/workers-types@5.20261011.1)(@types/node@22.19.3) + zod: 4.6.5 + transitivePeerDependencies: + - '@cloudflare/workers-types' + - '@types/node' + - bufferutil + - utf-8-validate + '@cloudflare/workerd-darwin-64@1.20261006.1': optional: true @@ -6052,52 +4731,40 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@csstools/color-helpers@5.1.0': {} + '@csstools/color-helpers@6.1.2': {} - '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-calc@3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': dependencies: - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 - '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-color-parser@4.2.6(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': dependencies: - '@csstools/color-helpers': 5.1.0 - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 + '@csstools/color-helpers': 6.1.2 + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 - '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2)': dependencies: - '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-tokenizer': 4.0.2 - '@csstools/css-tokenizer@3.0.4': {} + '@csstools/css-syntax-patches-for-csstree@1.1.15(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.2': {} '@date-fns/tz@1.4.1': {} '@drizzle-team/brocli@0.10.2': {} - '@emnapi/core@1.4.5': - dependencies: - '@emnapi/wasi-threads': 1.0.4 - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.7.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.0.4': - dependencies: - tslib: 2.8.1 - optional: true - - '@esbuild-kit/core-utils@3.3.2': + '@esbuild-kit/core-utils@3.3.2': dependencies: esbuild: 0.18.20 source-map-support: 0.5.21 @@ -6407,6 +5074,10 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true + '@exodus/bytes@1.16.0(@noble/hashes@2.4.0)': + optionalDependencies: + '@noble/hashes': 2.4.0 + '@faker-js/faker@10.1.0': {} '@floating-ui/core@1.7.3': @@ -6438,26 +5109,13 @@ snapshots: dependencies: react: 19.2.1 - '@img/colour@1.0.0': - optional: true - '@img/colour@1.1.0': {} - '@img/sharp-darwin-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - optional: true - '@img/sharp-darwin-arm64@0.35.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.3.4 optional: true - '@img/sharp-darwin-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 - optional: true - '@img/sharp-darwin-x64@0.35.5': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.3.4 @@ -6468,151 +5126,76 @@ snapshots: '@img/sharp-wasm32': 0.35.5 optional: true - '@img/sharp-libvips-darwin-arm64@1.2.4': - optional: true - '@img/sharp-libvips-darwin-arm64@1.3.4': optional: true - '@img/sharp-libvips-darwin-x64@1.2.4': - optional: true - '@img/sharp-libvips-darwin-x64@1.3.4': optional: true - '@img/sharp-libvips-linux-arm64@1.2.4': - optional: true - '@img/sharp-libvips-linux-arm64@1.3.4': optional: true - '@img/sharp-libvips-linux-arm@1.2.4': - optional: true - '@img/sharp-libvips-linux-arm@1.3.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.4': - optional: true - '@img/sharp-libvips-linux-ppc64@1.3.4': optional: true - '@img/sharp-libvips-linux-riscv64@1.2.4': - optional: true - '@img/sharp-libvips-linux-riscv64@1.3.4': optional: true - '@img/sharp-libvips-linux-s390x@1.2.4': - optional: true - '@img/sharp-libvips-linux-s390x@1.3.4': optional: true - '@img/sharp-libvips-linux-x64@1.2.4': - optional: true - '@img/sharp-libvips-linux-x64@1.3.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.4': optional: true - '@img/sharp-linux-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 - optional: true - '@img/sharp-linux-arm64@0.35.5': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.3.4 optional: true - '@img/sharp-linux-arm@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 - optional: true - '@img/sharp-linux-arm@0.35.5': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.3.4 optional: true - '@img/sharp-linux-ppc64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 - optional: true - '@img/sharp-linux-ppc64@0.35.5': optionalDependencies: '@img/sharp-libvips-linux-ppc64': 1.3.4 optional: true - '@img/sharp-linux-riscv64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 - optional: true - '@img/sharp-linux-riscv64@0.35.5': optionalDependencies: '@img/sharp-libvips-linux-riscv64': 1.3.4 optional: true - '@img/sharp-linux-s390x@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 - optional: true - '@img/sharp-linux-s390x@0.35.5': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.3.4 optional: true - '@img/sharp-linux-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 - optional: true - '@img/sharp-linux-x64@0.35.5': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.3.4 optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - optional: true - '@img/sharp-linuxmusl-arm64@0.35.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 optional: true - '@img/sharp-linuxmusl-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - optional: true - '@img/sharp-linuxmusl-x64@0.35.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.3.4 optional: true - '@img/sharp-wasm32@0.34.5': - dependencies: - '@emnapi/runtime': 1.7.1 - optional: true - '@img/sharp-wasm32@0.35.5': dependencies: '@emnapi/runtime': 1.11.3 @@ -6623,248 +5206,15 @@ snapshots: '@img/sharp-wasm32': 0.35.5 optional: true - '@img/sharp-win32-arm64@0.34.5': - optional: true - '@img/sharp-win32-arm64@0.35.5': optional: true - '@img/sharp-win32-ia32@0.34.5': - optional: true - '@img/sharp-win32-ia32@0.35.5': optional: true - '@img/sharp-win32-x64@0.34.5': - optional: true - '@img/sharp-win32-x64@0.35.5': optional: true - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - - '@istanbuljs/load-nyc-config@1.1.0': - dependencies: - camelcase: 5.3.1 - find-up: 4.1.0 - get-package-type: 0.1.0 - js-yaml: 3.14.2 - resolve-from: 5.0.0 - - '@istanbuljs/schema@0.1.3': {} - - '@jest/console@30.3.0': - dependencies: - '@jest/types': 30.3.0 - '@types/node': 22.19.3 - chalk: 4.1.2 - jest-message-util: 30.3.0 - jest-util: 30.3.0 - slash: 3.0.0 - - '@jest/core@30.3.0(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.7.3))': - dependencies: - '@jest/console': 30.3.0 - '@jest/pattern': 30.0.1 - '@jest/reporters': 30.3.0 - '@jest/test-result': 30.3.0 - '@jest/transform': 30.3.0 - '@jest/types': 30.3.0 - '@types/node': 22.19.3 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 4.3.0 - exit-x: 0.2.2 - graceful-fs: 4.2.11 - jest-changed-files: 30.3.0 - jest-config: 30.3.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.7.3)) - jest-haste-map: 30.3.0 - jest-message-util: 30.3.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.3.0 - jest-resolve-dependencies: 30.3.0 - jest-runner: 30.3.0 - jest-runtime: 30.3.0 - jest-snapshot: 30.3.0 - jest-util: 30.3.0 - jest-validate: 30.3.0 - jest-watcher: 30.3.0 - pretty-format: 30.3.0 - slash: 3.0.0 - transitivePeerDependencies: - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - - '@jest/diff-sequences@30.3.0': {} - - '@jest/environment-jsdom-abstract@30.3.0(jsdom@26.1.0)': - dependencies: - '@jest/environment': 30.3.0 - '@jest/fake-timers': 30.3.0 - '@jest/types': 30.3.0 - '@types/jsdom': 21.1.7 - '@types/node': 22.19.3 - jest-mock: 30.3.0 - jest-util: 30.3.0 - jsdom: 26.1.0 - - '@jest/environment@30.3.0': - dependencies: - '@jest/fake-timers': 30.3.0 - '@jest/types': 30.3.0 - '@types/node': 22.19.3 - jest-mock: 30.3.0 - - '@jest/expect-utils@29.7.0': - dependencies: - jest-get-type: 29.6.3 - - '@jest/expect-utils@30.3.0': - dependencies: - '@jest/get-type': 30.1.0 - - '@jest/expect@30.3.0': - dependencies: - expect: 30.3.0 - jest-snapshot: 30.3.0 - transitivePeerDependencies: - - supports-color - - '@jest/fake-timers@30.3.0': - dependencies: - '@jest/types': 30.3.0 - '@sinonjs/fake-timers': 15.2.2 - '@types/node': 22.19.3 - jest-message-util: 30.3.0 - jest-mock: 30.3.0 - jest-util: 30.3.0 - - '@jest/get-type@30.1.0': {} - - '@jest/globals@30.3.0': - dependencies: - '@jest/environment': 30.3.0 - '@jest/expect': 30.3.0 - '@jest/types': 30.3.0 - jest-mock: 30.3.0 - transitivePeerDependencies: - - supports-color - - '@jest/pattern@30.0.1': - dependencies: - '@types/node': 22.19.3 - jest-regex-util: 30.0.1 - - '@jest/reporters@30.3.0': - dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 30.3.0 - '@jest/test-result': 30.3.0 - '@jest/transform': 30.3.0 - '@jest/types': 30.3.0 - '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 22.19.3 - chalk: 4.1.2 - collect-v8-coverage: 1.0.2 - exit-x: 0.2.2 - glob: 10.5.0 - graceful-fs: 4.2.11 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-instrument: 6.0.3 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 - istanbul-reports: 3.1.7 - jest-message-util: 30.3.0 - jest-util: 30.3.0 - jest-worker: 30.3.0 - slash: 3.0.0 - string-length: 4.0.2 - v8-to-istanbul: 9.3.0 - transitivePeerDependencies: - - supports-color - - '@jest/schemas@29.6.3': - dependencies: - '@sinclair/typebox': 0.27.8 - - '@jest/schemas@30.0.5': - dependencies: - '@sinclair/typebox': 0.34.40 - - '@jest/snapshot-utils@30.3.0': - dependencies: - '@jest/types': 30.3.0 - chalk: 4.1.2 - graceful-fs: 4.2.11 - natural-compare: 1.4.0 - - '@jest/source-map@30.0.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - callsites: 3.1.0 - graceful-fs: 4.2.11 - - '@jest/test-result@30.3.0': - dependencies: - '@jest/console': 30.3.0 - '@jest/types': 30.3.0 - '@types/istanbul-lib-coverage': 2.0.6 - collect-v8-coverage: 1.0.2 - - '@jest/test-sequencer@30.3.0': - dependencies: - '@jest/test-result': 30.3.0 - graceful-fs: 4.2.11 - jest-haste-map: 30.3.0 - slash: 3.0.0 - - '@jest/transform@30.3.0': - dependencies: - '@babel/core': 7.28.3 - '@jest/types': 30.3.0 - '@jridgewell/trace-mapping': 0.3.31 - babel-plugin-istanbul: 7.0.1 - chalk: 4.1.2 - convert-source-map: 2.0.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.11 - jest-haste-map: 30.3.0 - jest-regex-util: 30.0.1 - jest-util: 30.3.0 - pirates: 4.0.7 - slash: 3.0.0 - write-file-atomic: 5.0.1 - transitivePeerDependencies: - - supports-color - - '@jest/types@29.6.3': - dependencies: - '@jest/schemas': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 22.19.3 - '@types/yargs': 17.0.33 - chalk: 4.1.2 - - '@jest/types@30.3.0': - dependencies: - '@jest/pattern': 30.0.1 - '@jest/schemas': 30.0.5 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 22.19.3 - '@types/yargs': 17.0.33 - chalk: 4.1.2 - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6891,13 +5241,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@napi-rs/wasm-runtime@0.2.12': - dependencies: - '@emnapi/core': 1.4.5 - '@emnapi/runtime': 1.7.1 - '@tybys/wasm-util': 0.10.0 - optional: true - '@next/env@15.5.27': {} '@next/swc-darwin-arm64@15.5.27': @@ -7019,10 +5362,9 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.57.0': optional: true - '@pkgjs/parseargs@0.11.0': - optional: true - - '@pkgr/core@0.2.9': {} + '@playwright/test@1.64.0': + dependencies: + playwright: 1.64.0 '@poppinss/colors@4.1.6': dependencies: @@ -7922,20 +6264,8 @@ snapshots: fflate: 0.7.3 string.prototype.codepointat: 0.2.1 - '@sinclair/typebox@0.27.8': {} - - '@sinclair/typebox@0.34.40': {} - '@sindresorhus/is@7.2.0': {} - '@sinonjs/commons@3.0.1': - dependencies: - type-detect: 4.0.8 - - '@sinonjs/fake-timers@15.2.2': - dependencies: - '@sinonjs/commons': 3.0.1 - '@speed-highlight/core@1.2.24': {} '@standard-schema/spec@1.1.0': {} @@ -8037,11 +6367,11 @@ snapshots: '@trpc/server': 11.8.0(typescript@5.7.3) typescript: 5.7.3 - '@trpc/next@11.2.0(@tanstack/react-query@5.90.12(react@19.2.1))(@trpc/client@11.8.0(@trpc/server@11.8.0(typescript@5.7.3))(typescript@5.7.3))(@trpc/react-query@11.8.0(@tanstack/react-query@5.90.12(react@19.2.1))(@trpc/client@11.8.0(@trpc/server@11.8.0(typescript@5.7.3))(typescript@5.7.3))(@trpc/server@11.8.0(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3))(@trpc/server@11.8.0(typescript@5.7.3))(next@15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3)': + '@trpc/next@11.2.0(@tanstack/react-query@5.90.12(react@19.2.1))(@trpc/client@11.8.0(@trpc/server@11.8.0(typescript@5.7.3))(typescript@5.7.3))(@trpc/react-query@11.8.0(@tanstack/react-query@5.90.12(react@19.2.1))(@trpc/client@11.8.0(@trpc/server@11.8.0(typescript@5.7.3))(typescript@5.7.3))(@trpc/server@11.8.0(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3))(@trpc/server@11.8.0(typescript@5.7.3))(next@15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3)': dependencies: '@trpc/client': 11.8.0(@trpc/server@11.8.0(typescript@5.7.3))(typescript@5.7.3) '@trpc/server': 11.8.0(typescript@5.7.3) - next: 15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: 19.2.1 react-dom: 19.2.1(react@19.2.1) typescript: 5.7.3 @@ -8070,63 +6400,25 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@tybys/wasm-util@0.10.0': + '@types/better-sqlite3@9.6.0': dependencies: - tslib: 2.8.1 + '@types/node': 22.19.3 optional: true - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.7 - - '@types/babel__generator@7.27.0': + '@types/chai@5.2.3': dependencies: - '@babel/types': 7.28.2 + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 - - '@types/babel__traverse@7.20.7': - dependencies: - '@babel/types': 7.28.2 + '@types/deep-eql@4.0.2': {} - '@types/better-sqlite3@9.6.0': - dependencies: - '@types/node': 22.19.3 - optional: true + '@types/estree@1.0.9': {} '@types/hoist-non-react-statics@3.3.6': dependencies: '@types/react': 19.2.1 hoist-non-react-statics: 3.3.2 - '@types/istanbul-lib-coverage@2.0.6': {} - - '@types/istanbul-lib-report@3.0.3': - dependencies: - '@types/istanbul-lib-coverage': 2.0.6 - - '@types/istanbul-reports@3.0.4': - dependencies: - '@types/istanbul-lib-report': 3.0.3 - - '@types/jest@29.5.14': - dependencies: - expect: 29.7.0 - pretty-format: 29.7.0 - - '@types/jsdom@21.1.7': - dependencies: - '@types/node': 22.19.3 - '@types/tough-cookie': 4.0.5 - parse5: 7.3.0 - '@types/node@22.19.3': dependencies: undici-types: 6.21.0 @@ -8139,20 +6431,10 @@ snapshots: dependencies: csstype: 3.1.3 - '@types/stack-utils@2.0.3': {} - - '@types/tough-cookie@4.0.5': {} - '@types/web-push@3.6.4': dependencies: '@types/node': 22.19.3 - '@types/yargs-parser@21.0.3': {} - - '@types/yargs@17.0.33': - dependencies: - '@types/yargs-parser': 21.0.3 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251016.1': optional: true @@ -8184,78 +6466,17 @@ snapshots: '@typescript/native-preview-win32-arm64': 7.0.0-dev.20251016.1 '@typescript/native-preview-win32-x64': 7.0.0-dev.20251016.1 - '@ungap/structured-clone@1.3.0': {} - '@unpic/core@1.0.3': dependencies: unpic: 4.2.2 - '@unpic/react@1.0.2(next@15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + '@unpic/react@1.0.2(next@15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@unpic/core': 1.0.3 react: 19.2.1 react-dom: 19.2.1(react@19.2.1) optionalDependencies: - next: 15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) - - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - optional: true - - '@unrs/resolver-binding-android-arm64@1.11.1': - optional: true - - '@unrs/resolver-binding-darwin-arm64@1.11.1': - optional: true - - '@unrs/resolver-binding-darwin-x64@1.11.1': - optional: true - - '@unrs/resolver-binding-freebsd-x64@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - dependencies: - '@napi-rs/wasm-runtime': 0.2.12 - optional: true - - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - optional: true - - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - optional: true - - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - optional: true + next: 15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@vercel/og@1.0.3(@types/node@22.19.3)': dependencies: @@ -8266,10 +6487,10 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@vinext/cloudflare@1.1.0(@cloudflare/vite-plugin@1.63.1(@types/node@22.19.3)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(wrangler@4.149.0(@cloudflare/workers-types@5.20261011.1)(@types/node@22.19.3)))(vinext@1.1.0(@types/node@22.19.3)(@vitejs/plugin-react@6.1.2(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))(next@15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))': + '@vinext/cloudflare@1.1.0(@cloudflare/vite-plugin@1.63.1(@types/node@22.19.3)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(wrangler@4.149.0(@cloudflare/workers-types@5.20261011.1)(@types/node@22.19.3)))(vinext@1.1.0(@types/node@22.19.3)(@vitejs/plugin-react@6.1.2(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))(next@15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@cloudflare/workers-response-store': 1.1.0 - vinext: 1.1.0(@types/node@22.19.3)(@vitejs/plugin-react@6.1.2(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))(next@15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + vinext: 1.1.0(@types/node@22.19.3)(@vitejs/plugin-react@6.1.2(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))(next@15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) optionalDependencies: '@cloudflare/vite-plugin': 1.63.1(@types/node@22.19.3)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(wrangler@4.149.0(@cloudflare/workers-types@5.20261011.1)(@types/node@22.19.3)) @@ -8280,44 +6501,64 @@ snapshots: '@rolldown/pluginutils': 1.0.1 vite: 8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) - acorn-walk@8.3.4: + '@vitest/expect@4.1.11': dependencies: - acorn: 8.14.1 + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + chai: 6.3.0 + tinyrainbow: 3.2.0 - acorn@8.14.1: {} + '@vitest/mocker@4.1.11(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) - agent-base@7.1.3: {} + '@vitest/pretty-format@4.1.11': + dependencies: + tinyrainbow: 3.2.0 - ansi-escapes@4.3.2: + '@vitest/runner@4.1.11': dependencies: - type-fest: 0.21.3 + '@vitest/utils': 4.1.11 + pathe: 2.0.3 - ansi-escapes@7.2.0: + '@vitest/snapshot@4.1.11': dependencies: - environment: 1.1.0 + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 - ansi-regex@5.0.1: {} + '@vitest/spy@4.1.11': {} - ansi-regex@6.2.2: {} + '@vitest/utils@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.2.0 - ansi-styles@4.3.0: + acorn-walk@8.3.4: dependencies: - color-convert: 2.0.1 + acorn: 8.14.1 - ansi-styles@5.2.0: {} + acorn@8.14.1: {} - ansi-styles@6.2.3: {} + agent-base@7.1.3: {} - anymatch@3.1.3: + ansi-escapes@7.2.0: dependencies: - normalize-path: 3.0.0 - picomatch: 4.0.4 + environment: 1.1.0 - arg@4.1.3: {} + ansi-regex@6.2.2: {} - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 + ansi-styles@6.2.3: {} + + arg@4.1.3: {} aria-hidden@1.2.6: dependencies: @@ -8330,6 +6571,8 @@ snapshots: minimalistic-assert: 1.0.1 safer-buffer: 2.1.2 + assertion-error@2.0.1: {} + asynckit@0.4.0: {} axios@1.16.0: @@ -8340,65 +6583,14 @@ snapshots: transitivePeerDependencies: - debug - babel-jest@30.3.0(@babel/core@7.28.3): - dependencies: - '@babel/core': 7.28.3 - '@jest/transform': 30.3.0 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 7.0.1 - babel-preset-jest: 30.3.0(@babel/core@7.28.3) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-istanbul@7.0.1: - dependencies: - '@babel/helper-plugin-utils': 7.27.1 - '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-instrument: 6.0.3 - test-exclude: 6.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-jest-hoist@30.3.0: - dependencies: - '@types/babel__core': 7.20.5 - - babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.3): - dependencies: - '@babel/core': 7.28.3 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.3) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.3) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.3) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.3) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.3) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.3) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.3) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.3) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.3) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.3) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.3) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.3) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.3) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.3) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.3) - - babel-preset-jest@30.3.0(@babel/core@7.28.3): - dependencies: - '@babel/core': 7.28.3 - babel-plugin-jest-hoist: 30.3.0 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.3) - balanced-match@1.0.2: {} base64-js@0.0.8: {} - baseline-browser-mapping@2.9.9: {} + baseline-browser-mapping@2.9.9: + optional: true - better-auth@1.7.7(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261011.1)(@types/better-sqlite3@9.6.0)(better-sqlite3@13.0.3)(kysely@0.29.6))(next@15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + better-auth@1.7.7(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261011.1)(@types/better-sqlite3@9.6.0)(better-sqlite3@13.0.3)(kysely@0.29.6))(next@15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vitest@4.1.11(@types/node@22.19.3)(jsdom@30.1.2(@noble/hashes@2.4.0))(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))): dependencies: '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@3.25.76))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5) '@better-auth/drizzle-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@3.25.76))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.4(@cloudflare/workers-types@5.20261011.1)(@types/better-sqlite3@9.6.0)(better-sqlite3@13.0.3)(kysely@0.29.6)) @@ -8420,9 +6612,10 @@ snapshots: optionalDependencies: drizzle-kit: 0.31.11 drizzle-orm: 0.45.4(@cloudflare/workers-types@5.20261011.1)(@types/better-sqlite3@9.6.0)(better-sqlite3@13.0.3)(kysely@0.29.6) - next: 15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: 19.2.1 react-dom: 19.2.1(react@19.2.1) + vitest: 4.1.11(@types/node@22.19.3)(jsdom@30.1.2(@noble/hashes@2.4.0))(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - '@opentelemetry/api' @@ -8440,6 +6633,10 @@ snapshots: node-addon-api: 8.9.2 optional: true + bidi-js@1.1.0: + dependencies: + require-from-string: 2.0.2 + blake3-wasm@2.1.5: {} bn.js@5.2.3: {} @@ -8465,10 +6662,7 @@ snapshots: electron-to-chromium: 1.5.267 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) - - bser@2.1.1: - dependencies: - node-int64: 0.4.0 + optional: true buffer-equal-constant-time@1.0.1: {} @@ -8479,28 +6673,13 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - callsites@3.1.0: {} - - camelcase@5.3.1: {} - - camelcase@6.3.0: {} - camelize@1.0.1: {} caniuse-lite@1.0.30001760: {} - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - char-regex@1.0.2: {} - - ci-info@3.9.0: {} - - ci-info@4.3.0: {} + chai@6.3.0: {} - cjs-module-lexer@2.1.0: {} + cjs-module-lexer@1.2.3: {} class-variance-authority@0.7.1: dependencies: @@ -8517,12 +6696,6 @@ snapshots: client-only@0.0.1: {} - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - clsx@2.1.1: {} cmdk@1.1.1(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): @@ -8537,14 +6710,6 @@ snapshots: - '@types/react' - '@types/react-dom' - co@4.6.0: {} - - collect-v8-coverage@1.0.2: {} - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - color-name@1.1.4: {} colorette@2.0.20: {} @@ -8581,12 +6746,6 @@ snapshots: transitivePeerDependencies: - encoding - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - css-background-parser@0.1.0: {} css-box-shadow@1.0.0-3: {} @@ -8601,17 +6760,19 @@ snapshots: css-color-keywords: 1.0.0 postcss-value-parser: 4.2.0 - cssstyle@4.6.0: + css-tree@3.2.1: dependencies: - '@asamuzakjp/css-color': 3.2.0 - rrweb-cssom: 0.8.0 + mdn-data: 2.27.1 + source-map-js: 1.2.2 csstype@3.1.3: {} - data-urls@5.0.0: + data-urls@8.0.0(@noble/hashes@2.4.0): dependencies: - whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 + whatwg-mimetype: 5.0.0 + whatwg-url: 17.2.0(@noble/hashes@2.4.0) + transitivePeerDependencies: + - '@noble/hashes' date-fns-jalali@4.1.0-0: {} @@ -8623,11 +6784,7 @@ snapshots: dependencies: ms: 2.1.3 - decimal.js@10.5.0: {} - - dedent@1.6.0: {} - - deepmerge@4.3.1: {} + decimal.js@10.6.0: {} defu@6.1.7: {} @@ -8635,12 +6792,8 @@ snapshots: detect-libc@2.1.2: {} - detect-newline@3.1.0: {} - detect-node-es@1.1.0: {} - diff-sequences@29.6.3: {} - diff@4.0.4: {} dotenv@10.0.0: {} @@ -8665,37 +6818,26 @@ 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 - electron-to-chromium@1.5.267: {} - - emittery@0.13.1: {} + electron-to-chromium@1.5.267: + optional: true emoji-regex-xs@2.0.1: {} emoji-regex@10.6.0: {} - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - enhanced-resolve@5.18.4: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 - entities@6.0.0: {} + entities@8.1.0: {} environment@1.1.0: {} - error-ex@1.3.2: - dependencies: - is-arrayish: 0.2.1 - error-stack-parser-es@1.0.5: {} es-define-property@1.0.1: {} @@ -8704,6 +6846,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.3.2: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -8827,46 +6971,18 @@ snapshots: '@esbuild/win32-ia32': 0.28.2 '@esbuild/win32-x64': 0.28.2 - escalade@3.2.0: {} + escalade@3.2.0: + optional: true escape-html@1.0.3: {} - escape-string-regexp@2.0.0: {} - - esprima@4.0.1: {} - - eventemitter3@5.0.1: {} - - execa@5.1.1: + estree-walker@3.0.3: dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - - exit-x@0.2.2: {} + '@types/estree': 1.0.9 - expect@29.7.0: - dependencies: - '@jest/expect-utils': 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 + eventemitter3@5.0.1: {} - expect@30.3.0: - dependencies: - '@jest/expect-utils': 30.3.0 - '@jest/get-type': 30.1.0 - jest-matcher-utils: 30.3.0 - jest-message-util: 30.3.0 - jest-mock: 30.3.0 - jest-util: 30.3.0 + expect-type@1.4.0: {} fast-glob@3.3.3: dependencies: @@ -8876,16 +6992,10 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} - fastq@1.20.3: dependencies: reusify: 1.1.0 - fb-watchman@2.0.2: - dependencies: - bser: 2.1.1 - fdir@6.5.0(picomatch@4.0.7): optionalDependencies: picomatch: 4.0.7 @@ -8896,18 +7006,8 @@ snapshots: dependencies: to-regex-range: 5.0.1 - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - follow-redirects@1.16.0: {} - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -8916,17 +7016,11 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 - fs.realpath@1.0.0: {} - fsevents@2.3.3: optional: true function-bind@1.1.2: {} - gensync@1.0.0-beta.2: {} - - get-caller-file@2.0.5: {} - get-east-asian-width@1.4.0: {} get-intrinsic@1.3.0: @@ -8944,15 +7038,11 @@ snapshots: get-nonce@1.0.1: {} - get-package-type@0.1.0: {} - get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stream@6.0.1: {} - get-tsconfig@4.10.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -8961,549 +7051,99 @@ 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.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - glob@13.0.6: dependencies: minimatch: 9.0.9 minipass: 7.1.3 path-scurry: 2.0.2 - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 9.0.9 - once: 1.4.0 - path-is-absolute: 1.0.1 - gopd@1.2.0: {} - graceful-fs@4.2.11: {} - - harfbuzzjs@0.10.0: {} - - has-flag@4.0.0: {} - - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - hex-rgb@4.3.0: {} - - hoist-non-react-statics@3.3.2: - dependencies: - react-is: 16.13.1 - - html-encoding-sniffer@4.0.0: - dependencies: - whatwg-encoding: 3.1.1 - - html-escaper@2.0.2: {} - - html-parse-stringify@3.0.1: - dependencies: - void-elements: 3.1.0 - - http-proxy-agent@7.0.2: - dependencies: - agent-base: 7.1.3 - debug: 4.4.1 - transitivePeerDependencies: - - supports-color - - http_ece@1.2.0: {} - - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.3 - debug: 4.4.1 - transitivePeerDependencies: - - supports-color - - human-signals@2.1.0: {} - - husky@9.1.7: {} - - i18next-browser-languagedetector@8.2.1: - dependencies: - '@babel/runtime': 7.28.4 - - i18next-fs-backend@2.6.0: {} - - i18next-http-backend@3.0.6: - dependencies: - cross-fetch: 4.1.0 - transitivePeerDependencies: - - encoding - - i18next@25.10.10(typescript@5.7.3): - dependencies: - '@babel/runtime': 7.29.2 - optionalDependencies: - typescript: 5.7.3 - - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - - idb@8.0.3: {} - - import-local@3.2.0: - dependencies: - pkg-dir: 4.2.0 - resolve-cwd: 3.0.0 - - imurmurhash@0.1.4: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - input-otp@1.4.2(react-dom@19.2.1(react@19.2.1))(react@19.2.1): - dependencies: - react: 19.2.1 - react-dom: 19.2.1(react@19.2.1) - - ipaddr.js@2.5.0: {} - - is-arrayish@0.2.1: {} - - is-extglob@2.1.1: {} - - is-fullwidth-code-point@3.0.0: {} - - is-fullwidth-code-point@5.1.0: - dependencies: - get-east-asian-width: 1.4.0 - - is-generator-fn@2.1.0: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-number@7.0.0: {} - - is-potential-custom-element-name@1.0.1: {} - - is-stream@2.0.1: {} - - is-what@5.5.0: {} - - isexe@2.0.0: {} - - istanbul-lib-coverage@3.2.2: {} - - istanbul-lib-instrument@6.0.3: - dependencies: - '@babel/core': 7.28.3 - '@babel/parser': 7.28.3 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.2 - semver: 7.7.3 - transitivePeerDependencies: - - supports-color - - istanbul-lib-report@3.0.1: - dependencies: - istanbul-lib-coverage: 3.2.2 - make-dir: 4.0.0 - supports-color: 7.2.0 - - istanbul-lib-source-maps@5.0.6: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - debug: 4.4.1 - istanbul-lib-coverage: 3.2.2 - transitivePeerDependencies: - - supports-color - - istanbul-reports@3.1.7: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 - - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - jest-changed-files@30.3.0: - dependencies: - execa: 5.1.1 - jest-util: 30.3.0 - p-limit: 3.1.0 - - jest-circus@30.3.0: - dependencies: - '@jest/environment': 30.3.0 - '@jest/expect': 30.3.0 - '@jest/test-result': 30.3.0 - '@jest/types': 30.3.0 - '@types/node': 22.19.3 - chalk: 4.1.2 - co: 4.6.0 - dedent: 1.6.0 - is-generator-fn: 2.1.0 - jest-each: 30.3.0 - jest-matcher-utils: 30.3.0 - jest-message-util: 30.3.0 - jest-runtime: 30.3.0 - jest-snapshot: 30.3.0 - jest-util: 30.3.0 - p-limit: 3.1.0 - pretty-format: 30.3.0 - pure-rand: 7.0.1 - slash: 3.0.0 - stack-utils: 2.0.6 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-cli@30.3.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.7.3)): - dependencies: - '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.7.3)) - '@jest/test-result': 30.3.0 - '@jest/types': 30.3.0 - chalk: 4.1.2 - exit-x: 0.2.2 - import-local: 3.2.0 - jest-config: 30.3.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.7.3)) - jest-util: 30.3.0 - jest-validate: 30.3.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - - jest-config@30.3.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.7.3)): - dependencies: - '@babel/core': 7.28.3 - '@jest/get-type': 30.1.0 - '@jest/pattern': 30.0.1 - '@jest/test-sequencer': 30.3.0 - '@jest/types': 30.3.0 - babel-jest: 30.3.0(@babel/core@7.28.3) - chalk: 4.1.2 - ci-info: 4.3.0 - deepmerge: 4.3.1 - glob: 10.5.0 - graceful-fs: 4.2.11 - jest-circus: 30.3.0 - jest-docblock: 30.2.0 - jest-environment-node: 30.3.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.3.0 - jest-runner: 30.3.0 - jest-util: 30.3.0 - jest-validate: 30.3.0 - parse-json: 5.2.0 - pretty-format: 30.3.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.19.3 - ts-node: 10.9.2(@types/node@22.19.3)(typescript@5.7.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-diff@29.7.0: - dependencies: - chalk: 4.1.2 - diff-sequences: 29.6.3 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-diff@30.3.0: - dependencies: - '@jest/diff-sequences': 30.3.0 - '@jest/get-type': 30.1.0 - chalk: 4.1.2 - pretty-format: 30.3.0 + graceful-fs@4.2.11: {} - jest-docblock@30.2.0: - dependencies: - detect-newline: 3.1.0 + harfbuzzjs@0.10.0: {} - jest-each@30.3.0: - dependencies: - '@jest/get-type': 30.1.0 - '@jest/types': 30.3.0 - chalk: 4.1.2 - jest-util: 30.3.0 - pretty-format: 30.3.0 + has-symbols@1.1.0: {} - jest-environment-jsdom@30.3.0: + has-tostringtag@1.0.2: dependencies: - '@jest/environment': 30.3.0 - '@jest/environment-jsdom-abstract': 30.3.0(jsdom@26.1.0) - jsdom: 26.1.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate + has-symbols: 1.1.0 - jest-environment-node@30.3.0: + hasown@2.0.2: dependencies: - '@jest/environment': 30.3.0 - '@jest/fake-timers': 30.3.0 - '@jest/types': 30.3.0 - '@types/node': 22.19.3 - jest-mock: 30.3.0 - jest-util: 30.3.0 - jest-validate: 30.3.0 + function-bind: 1.1.2 - jest-get-type@29.6.3: {} + hex-rgb@4.3.0: {} - jest-haste-map@30.3.0: + hoist-non-react-statics@3.3.2: dependencies: - '@jest/types': 30.3.0 - '@types/node': 22.19.3 - anymatch: 3.1.3 - fb-watchman: 2.0.2 - graceful-fs: 4.2.11 - jest-regex-util: 30.0.1 - jest-util: 30.3.0 - jest-worker: 30.3.0 - picomatch: 4.0.4 - walker: 1.0.8 - optionalDependencies: - fsevents: 2.3.3 + react-is: 16.13.1 - jest-leak-detector@30.3.0: + html-encoding-sniffer@7.0.0(@noble/hashes@2.4.0): dependencies: - '@jest/get-type': 30.1.0 - pretty-format: 30.3.0 + '@exodus/bytes': 1.16.0(@noble/hashes@2.4.0) + transitivePeerDependencies: + - '@noble/hashes' - jest-matcher-utils@29.7.0: + html-parse-stringify@3.0.1: dependencies: - chalk: 4.1.2 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 + void-elements: 3.1.0 - jest-matcher-utils@30.3.0: - dependencies: - '@jest/get-type': 30.1.0 - chalk: 4.1.2 - jest-diff: 30.3.0 - pretty-format: 30.3.0 + http_ece@1.2.0: {} - jest-message-util@29.7.0: + https-proxy-agent@7.0.6: dependencies: - '@babel/code-frame': 7.27.1 - '@jest/types': 29.6.3 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - stack-utils: 2.0.6 + agent-base: 7.1.3 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color - jest-message-util@30.3.0: - dependencies: - '@babel/code-frame': 7.27.1 - '@jest/types': 30.3.0 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - picomatch: 4.0.4 - pretty-format: 30.3.0 - slash: 3.0.0 - stack-utils: 2.0.6 + husky@9.1.7: {} - jest-mock@30.3.0: + i18next-browser-languagedetector@8.2.1: dependencies: - '@jest/types': 30.3.0 - '@types/node': 22.19.3 - jest-util: 30.3.0 - - jest-pnp-resolver@1.2.3(jest-resolve@30.3.0): - optionalDependencies: - jest-resolve: 30.3.0 + '@babel/runtime': 7.28.4 - jest-regex-util@30.0.1: {} + i18next-fs-backend@2.6.0: {} - jest-resolve-dependencies@30.3.0: + i18next-http-backend@3.0.6: dependencies: - jest-regex-util: 30.0.1 - jest-snapshot: 30.3.0 + cross-fetch: 4.1.0 transitivePeerDependencies: - - supports-color + - encoding - jest-resolve@30.3.0: + i18next@25.10.10(typescript@5.7.3): dependencies: - chalk: 4.1.2 - graceful-fs: 4.2.11 - jest-haste-map: 30.3.0 - jest-pnp-resolver: 1.2.3(jest-resolve@30.3.0) - jest-util: 30.3.0 - jest-validate: 30.3.0 - slash: 3.0.0 - unrs-resolver: 1.11.1 - - jest-runner@30.3.0: - dependencies: - '@jest/console': 30.3.0 - '@jest/environment': 30.3.0 - '@jest/test-result': 30.3.0 - '@jest/transform': 30.3.0 - '@jest/types': 30.3.0 - '@types/node': 22.19.3 - chalk: 4.1.2 - emittery: 0.13.1 - exit-x: 0.2.2 - graceful-fs: 4.2.11 - jest-docblock: 30.2.0 - jest-environment-node: 30.3.0 - jest-haste-map: 30.3.0 - jest-leak-detector: 30.3.0 - jest-message-util: 30.3.0 - jest-resolve: 30.3.0 - jest-runtime: 30.3.0 - jest-util: 30.3.0 - jest-watcher: 30.3.0 - jest-worker: 30.3.0 - p-limit: 3.1.0 - source-map-support: 0.5.13 - transitivePeerDependencies: - - supports-color + '@babel/runtime': 7.29.2 + optionalDependencies: + typescript: 5.7.3 - jest-runtime@30.3.0: - dependencies: - '@jest/environment': 30.3.0 - '@jest/fake-timers': 30.3.0 - '@jest/globals': 30.3.0 - '@jest/source-map': 30.0.1 - '@jest/test-result': 30.3.0 - '@jest/transform': 30.3.0 - '@jest/types': 30.3.0 - '@types/node': 22.19.3 - chalk: 4.1.2 - cjs-module-lexer: 2.1.0 - collect-v8-coverage: 1.0.2 - glob: 10.5.0 - graceful-fs: 4.2.11 - jest-haste-map: 30.3.0 - jest-message-util: 30.3.0 - jest-mock: 30.3.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.3.0 - jest-snapshot: 30.3.0 - jest-util: 30.3.0 - slash: 3.0.0 - strip-bom: 4.0.0 - transitivePeerDependencies: - - supports-color + idb@8.0.3: {} - jest-snapshot@30.3.0: - dependencies: - '@babel/core': 7.28.3 - '@babel/generator': 7.28.3 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.3) - '@babel/types': 7.28.2 - '@jest/expect-utils': 30.3.0 - '@jest/get-type': 30.1.0 - '@jest/snapshot-utils': 30.3.0 - '@jest/transform': 30.3.0 - '@jest/types': 30.3.0 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.3) - chalk: 4.1.2 - expect: 30.3.0 - graceful-fs: 4.2.11 - jest-diff: 30.3.0 - jest-matcher-utils: 30.3.0 - jest-message-util: 30.3.0 - jest-util: 30.3.0 - pretty-format: 30.3.0 - semver: 7.7.3 - synckit: 0.11.11 - transitivePeerDependencies: - - supports-color + inherits@2.0.4: {} - jest-util@29.7.0: + input-otp@1.4.2(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: - '@jest/types': 29.6.3 - '@types/node': 22.19.3 - chalk: 4.1.2 - ci-info: 3.9.0 - graceful-fs: 4.2.11 - picomatch: 4.0.4 + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) - jest-util@30.3.0: - dependencies: - '@jest/types': 30.3.0 - '@types/node': 22.19.3 - chalk: 4.1.2 - ci-info: 4.3.0 - graceful-fs: 4.2.11 - picomatch: 4.0.4 + ipaddr.js@2.5.0: {} - jest-validate@30.3.0: - dependencies: - '@jest/get-type': 30.1.0 - '@jest/types': 30.3.0 - camelcase: 6.3.0 - chalk: 4.1.2 - leven: 3.1.0 - pretty-format: 30.3.0 + is-extglob@2.1.1: {} - jest-watcher@30.3.0: + is-fullwidth-code-point@5.1.0: dependencies: - '@jest/test-result': 30.3.0 - '@jest/types': 30.3.0 - '@types/node': 22.19.3 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - emittery: 0.13.1 - jest-util: 30.3.0 - string-length: 4.0.2 + get-east-asian-width: 1.4.0 - jest-worker@30.3.0: + is-glob@4.0.3: dependencies: - '@types/node': 22.19.3 - '@ungap/structured-clone': 1.3.0 - jest-util: 30.3.0 - merge-stream: 2.0.0 - supports-color: 8.1.1 + is-extglob: 2.1.1 - jest@30.3.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.7.3)): - dependencies: - '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.7.3)) - '@jest/types': 30.3.0 - import-local: 3.2.0 - jest-cli: 30.3.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.7.3)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node + is-number@7.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-what@5.5.0: {} jiti@2.6.1: {} @@ -9511,43 +7151,30 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.14.2: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - - jsdom@26.1.0: - dependencies: - cssstyle: 4.6.0 - data-urls: 5.0.0 - decimal.js: 10.5.0 - html-encoding-sniffer: 4.0.0 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + jsdom@30.1.2(@noble/hashes@2.4.0): + dependencies: + '@asamuzakjp/css-color': 7.1.5 + '@asamuzakjp/dom-selector': 9.2.4 + '@bramus/specificity': 2.4.3 + '@csstools/css-syntax-patches-for-csstree': 1.1.15(css-tree@3.2.1) + '@exodus/bytes': 1.16.0(@noble/hashes@2.4.0) + css-tree: 3.2.1 + data-urls: 8.0.0(@noble/hashes@2.4.0) + decimal.js: 10.6.0 + html-encoding-sniffer: 7.0.0(@noble/hashes@2.4.0) is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.20 - parse5: 7.3.0 - rrweb-cssom: 0.8.0 + lru-cache: 11.5.3 + parse5: 8.0.1 saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 5.1.2 - w3c-xmlserializer: 5.0.0 - webidl-conversions: 7.0.0 - whatwg-encoding: 3.1.1 - whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 - ws: 8.18.2 + tough-cookie: 6.0.2 + undici: 8.11.2 + w3c-xmlserializer: 6.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 17.2.0(@noble/hashes@2.4.0) xml-name-validator: 5.0.0 transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - jsesc@3.1.0: {} - - json-parse-even-better-errors@2.3.1: {} - - json5@2.2.3: {} + - '@noble/hashes' jwa@2.0.1: dependencies: @@ -9566,8 +7193,6 @@ snapshots: kysely@0.29.6: {} - leven@3.1.0: {} - lightningcss-android-arm64@1.30.2: optional: true @@ -9671,8 +7296,6 @@ snapshots: base64-js: 0.0.8 unicode-trie: 2.0.0 - lines-and-columns@1.2.4: {} - lint-staged@16.4.0: dependencies: commander: 14.0.3 @@ -9691,10 +7314,6 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.2 - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - log-update@6.1.0: dependencies: ansi-escapes: 7.2.0 @@ -9707,14 +7326,8 @@ snapshots: dependencies: js-tokens: 4.0.0 - lru-cache@10.4.3: {} - lru-cache@11.5.3: {} - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - lucide-react@0.544.0(react@19.2.1): dependencies: react: 19.2.1 @@ -9725,19 +7338,11 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - make-dir@4.0.0: - dependencies: - semver: 7.7.3 - make-error@1.3.6: {} - makeerror@1.0.12: - dependencies: - tmpl: 1.0.5 - math-intrinsics@1.1.0: {} - merge-stream@2.0.0: {} + mdn-data@2.27.1: {} merge2@1.4.1: {} @@ -9752,8 +7357,6 @@ snapshots: dependencies: mime-db: 1.52.0 - mimic-fn@2.1.0: {} - mimic-function@5.0.1: {} miniflare@5.20261006.1-alpha(@types/node@22.19.3): @@ -9777,8 +7380,6 @@ snapshots: minimist@1.2.8: {} - minipass@7.1.2: {} - minipass@7.1.3: {} ms@2.1.3: {} @@ -9791,11 +7392,7 @@ snapshots: nanostores@1.5.5: {} - napi-postinstall@0.3.3: {} - - natural-compare@1.4.0: {} - - next-i18next@15.4.3(i18next@25.10.10(typescript@5.7.3))(next@15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-i18next@15.7.4(i18next@25.10.10(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3))(react@19.2.1): + next-i18next@15.4.3(i18next@25.10.10(typescript@5.7.3))(next@15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-i18next@15.7.4(i18next@25.10.10(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3))(react@19.2.1): dependencies: '@babel/runtime': 7.28.4 '@types/hoist-non-react-statics': 3.3.6 @@ -9803,7 +7400,7 @@ snapshots: hoist-non-react-statics: 3.3.2 i18next: 25.10.10(typescript@5.7.3) i18next-fs-backend: 2.6.0 - next: 15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: 19.2.1 react-i18next: 15.7.4(i18next@25.10.10(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3) @@ -9812,7 +7409,7 @@ snapshots: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) - next@15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + next@15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: '@next/env': 15.5.27 '@swc/helpers': 0.5.15 @@ -9820,7 +7417,7 @@ snapshots: postcss: 8.4.31 react: 19.2.1 react-dom: 19.2.1(react@19.2.1) - styled-jsx: 5.1.6(@babel/core@7.28.3)(react@19.2.1) + styled-jsx: 5.1.6(react@19.2.1) optionalDependencies: '@next/swc-darwin-arm64': 15.5.27 '@next/swc-darwin-x64': 15.5.27 @@ -9830,9 +7427,11 @@ snapshots: '@next/swc-linux-x64-musl': 15.5.27 '@next/swc-win32-arm64-msvc': 15.5.27 '@next/swc-win32-x64-msvc': 15.5.27 - sharp: 0.34.5 + '@playwright/test': 1.64.0 + sharp: 0.35.5(@types/node@22.19.3) transitivePeerDependencies: - '@babel/core' + - '@types/node' - babel-plugin-macros node-addon-api@8.9.2: @@ -9842,9 +7441,8 @@ snapshots: dependencies: whatwg-url: 5.0.0 - node-int64@0.4.0: {} - - node-releases@2.0.27: {} + node-releases@2.0.27: + optional: true nordigen-node@1.4.1: dependencies: @@ -9853,25 +7451,11 @@ snapshots: transitivePeerDependencies: - debug - normalize-path@3.0.0: {} - normalize-wheel@1.0.1: {} - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - - nwsapi@2.2.20: {} - object-assign@4.1.1: {} - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 + obug@2.2.1: {} onetime@7.0.0: dependencies: @@ -9909,22 +7493,6 @@ snapshots: '@oxlint/binding-win32-x64-msvc': 1.57.0 oxlint-tsgolint: 0.18.1 - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - - p-try@2.2.0: {} - - package-json-from-dist@1.0.1: {} - pako@0.2.9: {} parse-css-color@0.2.1: @@ -9932,27 +7500,9 @@ snapshots: color-name: 1.1.4 hex-rgb: 4.3.0 - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.27.1 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - - parse5@7.3.0: - dependencies: - entities: 6.0.0 - - path-exists@4.0.0: {} - - path-is-absolute@1.0.1: {} - - path-key@3.1.1: {} - - path-scurry@1.11.1: + parse5@8.0.1: dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 + entities: 8.1.0 path-scurry@2.0.2: dependencies: @@ -9969,25 +7519,25 @@ snapshots: picomatch@4.0.7: {} - pirates@4.0.7: {} - - pkg-dir@4.2.0: - dependencies: - find-up: 4.1.0 - plaid@38.1.0: dependencies: axios: 1.16.0 transitivePeerDependencies: - debug + playwright-core@1.64.0: {} + + playwright@1.64.0: + dependencies: + playwright-core: 1.64.0 + postcss-value-parser@4.2.0: {} postcss@8.4.31: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.20 picocolors: 1.1.1 - source-map-js: 1.2.1 + source-map-js: 1.2.2 postcss@8.5.14: dependencies: @@ -10009,18 +7559,6 @@ snapshots: pretty-bytes@6.1.1: {} - pretty-format@29.7.0: - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 - - pretty-format@30.3.0: - dependencies: - '@jest/schemas': 30.0.5 - ansi-styles: 5.2.0 - react-is: 18.3.1 - prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -10031,8 +7569,6 @@ snapshots: punycode@2.3.1: {} - pure-rand@7.0.1: {} - queue-microtask@1.2.3: {} radix-ui@1.4.3(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): @@ -10133,8 +7669,6 @@ snapshots: react-is@16.13.1: {} - react-is@18.3.1: {} - react-plaid-link@4.1.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: prop-types: 15.8.1 @@ -10181,13 +7715,7 @@ snapshots: react@19.2.1: {} - require-directory@2.1.1: {} - - resolve-cwd@3.0.0: - dependencies: - resolve-from: 5.0.0 - - resolve-from@5.0.0: {} + require-from-string@2.0.2: {} resolve-pkg-maps@1.0.0: {} @@ -10223,8 +7751,6 @@ snapshots: rou3@0.9.2: {} - rrweb-cssom@0.8.0: {} - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -10255,10 +7781,6 @@ snapshots: scheduler@0.27.0: {} - semver@6.3.1: {} - - semver@7.7.3: {} - semver@7.8.5: {} serwist@9.2.3(typescript@5.7.3): @@ -10278,38 +7800,6 @@ snapshots: set-cookie-parser@3.1.3: {} - sharp@0.34.5: - dependencies: - '@img/colour': 1.0.0 - detect-libc: 2.1.2 - semver: 7.7.3 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - optional: true - sharp@0.35.5(@types/node@22.19.3): dependencies: '@img/colour': 1.1.0 @@ -10343,18 +7833,10 @@ snapshots: '@img/sharp-win32-x64': 0.35.5 '@types/node': 22.19.3 - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - signal-exit@3.0.7: {} + siginfo@2.0.0: {} signal-exit@4.1.0: {} - slash@3.0.0: {} - slice-ansi@7.1.2: dependencies: ansi-styles: 6.2.3 @@ -10369,11 +7851,6 @@ snapshots: source-map-js@1.2.2: {} - source-map-support@0.5.13: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 @@ -10383,31 +7860,12 @@ snapshots: source-map@0.8.0: {} - sprintf-js@1.0.3: {} + stackback@0.0.2: {} - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 + std-env@4.3.0: {} string-argv@0.3.2: {} - string-length@4.0.2: - dependencies: - char-regex: 1.0.2 - strip-ansi: 6.0.1 - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - 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.1.2 - string-width@7.2.0: dependencies: emoji-regex: 10.6.0 @@ -10421,26 +7879,14 @@ snapshots: string.prototype.codepointat@0.2.1: {} - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - strip-ansi@7.1.2: dependencies: ansi-regex: 6.2.2 - strip-bom@4.0.0: {} - - strip-final-newline@2.0.0: {} - - strip-json-comments@3.1.1: {} - - styled-jsx@5.1.6(@babel/core@7.28.3)(react@19.2.1): + styled-jsx@5.1.6(react@19.2.1): dependencies: client-only: 0.0.1 react: 19.2.1 - optionalDependencies: - '@babel/core': 7.28.3 superjson@2.2.6: dependencies: @@ -10448,20 +7894,6 @@ snapshots: supports-color@10.2.2: {} - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - - symbol-tree@3.2.4: {} - - synckit@0.11.11: - dependencies: - '@pkgr/core': 0.2.9 - tagged-tag@1.0.0: {} tailwind-merge@2.6.0: {} @@ -10470,14 +7902,10 @@ snapshots: tapable@2.3.0: {} - test-exclude@6.0.0: - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 9.0.9 - tiny-inflate@1.0.3: {} + tinybench@2.9.0: {} + tinyexec@1.0.4: {} tinyglobby@0.2.17: @@ -10485,25 +7913,25 @@ snapshots: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 - tldts-core@6.1.86: {} + tinyrainbow@3.2.0: {} - tldts@6.1.86: - dependencies: - tldts-core: 6.1.86 + tldts-core@7.4.18: {} - tmpl@1.0.5: {} + tldts@7.4.18: + dependencies: + tldts-core: 7.4.18 to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - tough-cookie@5.1.2: + tough-cookie@6.0.2: dependencies: - tldts: 6.1.86 + tldts: 7.4.18 tr46@0.0.3: {} - tr46@5.1.1: + tr46@7.0.0: dependencies: punycode: 2.3.1 @@ -10534,10 +7962,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - type-detect@4.0.8: {} - - type-fest@0.21.3: {} - type-fest@5.10.0: dependencies: tagged-tag: 1.0.0 @@ -10548,6 +7972,8 @@ snapshots: undici@7.29.1: {} + undici@8.11.2: {} + unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 @@ -10559,35 +7985,12 @@ snapshots: unpic@4.2.2: {} - unrs-resolver@1.11.1: - dependencies: - napi-postinstall: 0.3.3 - optionalDependencies: - '@unrs/resolver-binding-android-arm-eabi': 1.11.1 - '@unrs/resolver-binding-android-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-x64': 1.11.1 - '@unrs/resolver-binding-freebsd-x64': 1.11.1 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 - '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-musl': 1.11.1 - '@unrs/resolver-binding-wasm32-wasi': 1.11.1 - '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 - '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 - '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 + optional: true use-callback-ref@1.3.3(@types/react@19.2.1)(react@19.2.1): dependencies: @@ -10612,12 +8015,6 @@ snapshots: v8-compile-cache-lib@3.0.1: {} - v8-to-istanbul@9.3.0: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - '@types/istanbul-lib-coverage': 2.0.6 - convert-source-map: 2.0.0 - vaul@1.1.2(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -10627,9 +8024,9 @@ snapshots: - '@types/react' - '@types/react-dom' - vinext@1.1.0(@types/node@22.19.3)(@vitejs/plugin-react@6.1.2(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))(next@15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): + vinext@1.1.0(@types/node@22.19.3)(@vitejs/plugin-react@6.1.2(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))(next@15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - '@unpic/react': 1.0.2(next@15.5.27(@babel/core@7.28.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@unpic/react': 1.0.2(next@15.5.27(@playwright/test@1.64.0)(@types/node@22.19.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@vercel/og': 1.0.3(@types/node@22.19.3) '@vinext/types': 1.0.1 '@vitejs/plugin-react': 6.1.2(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -10672,16 +8069,40 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 + vitest@4.1.11(@types/node@22.19.3)(jsdom@30.1.2(@noble/hashes@2.4.0))(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.2.1 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.3.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.2.0 + vite: 8.3.4(@types/node@22.19.3)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.3 + jsdom: 30.1.2(@noble/hashes@2.4.0) + transitivePeerDependencies: + - msw + void-elements@3.1.0: {} - w3c-xmlserializer@5.0.0: + w3c-xmlserializer@6.0.0: dependencies: xml-name-validator: 5.0.0 - walker@1.0.8: - dependencies: - makeerror: 1.0.12 - web-push@3.6.7: dependencies: asn1.js: 5.4.1 @@ -10696,27 +8117,27 @@ snapshots: webidl-conversions@3.0.1: {} - webidl-conversions@7.0.0: {} - - whatwg-encoding@3.1.1: - dependencies: - iconv-lite: 0.6.3 + webidl-conversions@8.0.1: {} - whatwg-mimetype@4.0.0: {} + whatwg-mimetype@5.0.0: {} - whatwg-url@14.2.0: + whatwg-url@17.2.0(@noble/hashes@2.4.0): dependencies: - tr46: 5.1.1 - webidl-conversions: 7.0.0 + '@exodus/bytes': 1.16.0(@noble/hashes@2.4.0) + tr46: 7.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' whatwg-url@5.0.0: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 - which@2.0.2: + why-is-node-running@2.3.0: dependencies: - isexe: 2.0.0 + siginfo: 2.0.0 + stackback: 0.0.2 workerd@1.20261006.1: optionalDependencies: @@ -10744,61 +8165,22 @@ snapshots: - bufferutil - utf-8-validate - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - 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.1.2 - wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 string-width: 7.2.0 strip-ansi: 7.1.2 - wrappy@1.0.2: {} - - write-file-atomic@5.0.1: - dependencies: - imurmurhash: 0.1.4 - signal-exit: 4.1.0 - - ws@8.18.2: {} - ws@8.21.0: {} xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} - y18n@5.0.8: {} - - yallist@3.1.1: {} - yaml@2.8.3: {} - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - yn@3.1.1: {} - yocto-queue@0.1.0: {} - yoga-layout@3.2.1: {} youch-core@0.3.3: diff --git a/src/lib/cron.test.ts b/src/lib/cron.test.ts deleted file mode 100644 index 0caf2a85b..000000000 --- a/src/lib/cron.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { cronFromBackend, cronToBackend } from '~/lib/cron'; - -const mockTimezoneOffset = (offset: number) => - jest.spyOn(Date.prototype, 'getTimezoneOffset').mockReturnValue(offset); - -describe('cron timezone conversions', () => { - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('shifts hours and minutes for negative timezone offsets', () => { - mockTimezoneOffset(-120); - - expect(cronToBackend('0 9 * * *')).toBe('0 7 * * *'); - }); - - it('shifts hours and minutes for positive offsets', () => { - mockTimezoneOffset(120); - - expect(cronToBackend('0 9 * * *')).toBe('0 11 * * *'); - }); - - it('handles minute underflow and day-of-week shift', () => { - mockTimezoneOffset(-90); - - expect(cronToBackend('15 0 * * 0')).toBe('45 22 * * 6'); - }); - - it('rolls month forward when crossing from the 31st', () => { - mockTimezoneOffset(120); - - expect(cronToBackend('0 23 31 1 *')).toBe('0 1 1 2 *'); - }); - - it('rolls month backward and converts to last-day marker', () => { - mockTimezoneOffset(-60); - - expect(cronToBackend('0 0 1 3 *')).toBe('0 23 $ 2 *'); - }); - - it('recovers from invalid day-of-month after timezone shift', () => { - mockTimezoneOffset(120); - - expect(cronToBackend('0 23 30 4 *')).toBe('0 1 1 5 *'); - }); - - it('restores last-day marker from backend notation', () => { - mockTimezoneOffset(0); - - expect(cronFromBackend('0 0 $ * *')).toBe('0 0 L * *'); - }); - - it('restores last-day marker from frontend notation', () => { - mockTimezoneOffset(0); - - expect(cronToBackend('0 0 L * *')).toBe('0 0 $ * *'); - }); - - it('round-trips frontend to backend and back', () => { - mockTimezoneOffset(60); - const expression = '15 5 * * 1'; - - expect(cronFromBackend(cronToBackend(expression))).toBe(expression); - }); -}); diff --git a/src/server/api/routers/expense.ts b/src/server/api/routers/expense.ts index 81fbd3321..6af5eab77 100644 --- a/src/server/api/routers/expense.ts +++ b/src/server/api/routers/expense.ts @@ -22,6 +22,7 @@ import { balanceView, expenseParticipants, expenses as expensesTable, + groupUsers, groups, } from '~/server/db/schema'; import { BigMath, currencyConversion } from '~/utils/numbers'; @@ -190,16 +191,7 @@ export const expenseRouter = createTRPCRouter({ } if (input.groupId !== null) { - const group = await db.query.groups.findFirst({ - where: eq(groups.id, input.groupId), - columns: { archivedAt: true }, - }); - if (!group) { - throw new TRPCError({ code: 'BAD_REQUEST', message: 'Group not found' }); - } - if (group.archivedAt) { - throw new TRPCError({ code: 'BAD_REQUEST', message: 'Group is archived' }); - } + await assertCanWriteToGroup(input.groupId, ctx.session.user.id); } try { @@ -225,6 +217,13 @@ export const expenseRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const { amount, rate, from, to, senderId, receiverId, groupId, expenseId } = input; + if (expenseId) { + await validateEditExpensePermission(expenseId, ctx.session.user.id); + } + if (groupId !== null && groupId !== undefined) { + await assertCanWriteToGroup(groupId, ctx.session.user.id); + } + if (!isCurrencyCode(from) || !isCurrencyCode(to)) { throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid currency code' }); } @@ -323,7 +322,7 @@ export const expenseRouter = createTRPCRouter({ getExpenseDetails: protectedProcedure .input(z.object({ expenseId: z.string() })) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { const expense = await db.query.expenses.findFirst({ where: eq(expensesTable.id, input.expenseId), with: { @@ -339,6 +338,10 @@ export const expenseRouter = createTRPCRouter({ }, }); + if (expense && !(await canAccessExpense(expense.id, ctx.session.user.id))) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Expense not found' }); + } + if (expense && expense.groupId !== null) { const participantIds = expense.expenseParticipants.map((ep) => ep.userId); const missingGroupMembers = await db.query.groups.findFirst({ @@ -445,16 +448,7 @@ export const expenseRouter = createTRPCRouter({ } if (expense.groupId !== null) { - const group = await db.query.groups.findFirst({ - where: eq(groups.id, expense.groupId), - columns: { archivedAt: true }, - }); - if (!group) { - throw new TRPCError({ code: 'BAD_REQUEST', message: 'Group not found' }); - } - if (group.archivedAt) { - throw new TRPCError({ code: 'BAD_REQUEST', message: 'Group is archived' }); - } + await assertCanWriteToGroup(expense.groupId, ctx.session.user.id); } await deleteExpense(input.expenseId, ctx.session.user.id); @@ -505,21 +499,40 @@ export const expenseRouter = createTRPCRouter({ }), }); -const validateEditExpensePermission = async (expenseId: string, userId: number): Promise => { - const [expenseParticipant, addedBy] = await Promise.all([ +/** + * Who may see or change an expense: its payer, the user who added it, a participant, or (for + * group expenses) any member of the group. + */ +const canAccessExpense = async (expenseId: string, userId: number): Promise => { + const expense = await db.query.expenses.findFirst({ + where: eq(expensesTable.id, expenseId), + columns: { paidBy: true, addedBy: true, groupId: true }, + }); + if (!expense) { + return false; + } + if (expense.paidBy === userId || expense.addedBy === userId) { + return true; + } + + const [participant, member] = await Promise.all([ db.query.expenseParticipants.findFirst({ where: and( eq(expenseParticipants.expenseId, expenseId), eq(expenseParticipants.userId, userId), ), }), - db.query.expenses.findFirst({ - where: eq(expensesTable.id, expenseId), - columns: { addedBy: true }, - }), + expense.groupId === null + ? undefined + : db.query.groupUsers.findFirst({ + where: and(eq(groupUsers.groupId, expense.groupId), eq(groupUsers.userId, userId)), + }), ]); + return Boolean(participant ?? member); +}; - if (!expenseParticipant && !addedBy?.addedBy) { +const validateEditExpensePermission = async (expenseId: string, userId: number): Promise => { + if (!(await canAccessExpense(expenseId, userId))) { throw new TRPCError({ code: 'UNAUTHORIZED', message: 'You are not the participant of the expense', @@ -527,6 +540,26 @@ const validateEditExpensePermission = async (expenseId: string, userId: number): } }; +/** The group must exist, be active, and the user must be a member. */ +const assertCanWriteToGroup = async (groupId: number, userId: number): Promise => { + const group = await db.query.groups.findFirst({ + where: eq(groups.id, groupId), + columns: { archivedAt: true }, + }); + if (!group) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Group not found' }); + } + const membership = await db.query.groupUsers.findFirst({ + where: and(eq(groupUsers.groupId, groupId), eq(groupUsers.userId, userId)), + }); + if (!membership) { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Not a group member' }); + } + if (group.archivedAt) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Group is archived' }); + } +}; + const findUserRecurrence = (recurrenceId: number, userId: number) => db.query.expenseRecurrences.findFirst({ where: (r) => eq(r.id, recurrenceId), diff --git a/src/server/api/services/splitService.ts b/src/server/api/services/splitService.ts index 73ccd77d7..525f23a0b 100644 --- a/src/server/api/services/splitService.ts +++ b/src/server/api/services/splitService.ts @@ -2,7 +2,7 @@ import { and, eq, inArray, sql } from 'drizzle-orm'; import type { BatchItem } from 'drizzle-orm/batch'; import { nanoid } from 'nanoid'; -import { db } from '~/server/db'; +import { bindings, db } from '~/server/db'; import { chunk, runBatch } from '~/server/db/batch'; import { SplitType, @@ -266,28 +266,41 @@ export async function runRecurringJob(jobid: number, now: Date = new Date()) { const newId = await deterministicUuid(`${jobid}:${slot.getTime()}`); const ts = now.getTime(); - await runBatch([ - db.run(sql` - INSERT OR IGNORE INTO "Expense" ( + // Drizzle's `db.run()` cannot be part of a D1 batch, so the whole batch is built from prepared + // D1 statements. Drizzle builders contribute their SQL via `toSQL()`. + const prepare = (statement: { sql: string; params: unknown[] }) => + bindings.DB.prepare(statement.sql).bind(...statement.params); + + await bindings.DB.batch([ + bindings.DB.prepare( + `INSERT OR IGNORE INTO "Expense" ( "id", "paidBy", "addedBy", "name", "category", "amount", "splitType", "expenseDate", "createdAt", "updatedAt", "currency", "groupId", "updatedBy", "recurrenceId" ) - SELECT ${newId}, "paidBy", "addedBy", "name", "category", "amount", "splitType", ${ts}, - ${ts}, ${ts}, "currency", "groupId", "updatedBy", "recurrenceId" - FROM "Expense" WHERE "id" = ${templateId}`), - db.run(sql` - INSERT OR IGNORE INTO "ExpenseParticipant" ("expenseId", "userId", "amount") - SELECT ${newId}, "userId", "amount" FROM "ExpenseParticipant" - WHERE "expenseId" = ${templateId} - AND EXISTS (SELECT 1 FROM "Expense" WHERE "id" = ${newId})`), - db - .update(expenseRecurrences) - .set({ notified: false }) - .where(eq(expenseRecurrences.jobId, jobid)), - db - .update(jobs) - .set({ lastRunAt: now, nextRunAt: nextRunAt(job.schedule, now) }) - .where(eq(jobs.jobid, jobid)), + SELECT ?, "paidBy", "addedBy", "name", "category", "amount", "splitType", ?, + ?, ?, "currency", "groupId", "updatedBy", "recurrenceId" + FROM "Expense" WHERE "id" = ?`, + ).bind(newId, ts, ts, ts, templateId), + bindings.DB.prepare( + `INSERT OR IGNORE INTO "ExpenseParticipant" ("expenseId", "userId", "amount") + SELECT ?, "userId", "amount" FROM "ExpenseParticipant" + WHERE "expenseId" = ? + AND EXISTS (SELECT 1 FROM "Expense" WHERE "id" = ?)`, + ).bind(newId, templateId, newId), + prepare( + db + .update(expenseRecurrences) + .set({ notified: false }) + .where(eq(expenseRecurrences.jobId, jobid)) + .toSQL(), + ), + prepare( + db + .update(jobs) + .set({ lastRunAt: now, nextRunAt: nextRunAt(job.schedule, now) }) + .where(eq(jobs.jobid, jobid)) + .toSQL(), + ), ]); return { skipped: false, expenseId: newId }; diff --git a/src/tests/addStore.test.ts b/src/tests/addStore.test.ts deleted file mode 100644 index 98a4b6b08..000000000 --- a/src/tests/addStore.test.ts +++ /dev/null @@ -1,1190 +0,0 @@ -import { SplitType, type User } from '~/server/db/types'; - -import { - type AddExpenseState, - type Participant, - calculateParticipantSplit, - calculateSplitShareBasedOnAmount, - initSplitShares, - useAddExpenseStore, -} from '~/store/addStore'; -import { getCurrencyHelpers } from '~/utils/numbers'; - -// Mock dependencies -jest.mock('~/utils/array', () => ({ - shuffleArray: jest.fn((arr: T[]): T[] => arr), // No shuffling for predictable tests -})); - -// Create mock users for testing -const createMockUser = (id: number, name: string, email: string): User => ({ - id, - name, - email, - currency: 'USD', - defaultCurrency: null, - emailVerified: null, - image: null, - preferredLanguage: 'en', - obapiProviderId: null, - bankingId: null, - hiddenFriendIds: [], -}); - -const user1: User = createMockUser(1, 'Alice', 'alice@example.com'); -const user2: User = createMockUser(2, 'Bob', 'bob@example.com'); -const user3: User = createMockUser(3, 'Charlie', 'charlie@example.com'); - -// Create participants with initial amounts -const createParticipants = (users: User[], amounts: bigint[] = []): Participant[] => - users.map((user, index) => ({ - ...user, - amount: amounts[index] ?? 0n, - })); - -// Helper to create split shares structure -const createSplitShares = (participants: Participant[], splitType: SplitType, shares: bigint[]) => { - const splitShares: Record> = {}; - - participants.forEach((participant, index) => { - splitShares[participant.id] = initSplitShares(); - splitShares[participant.id][splitType] = shares[index] ?? 0n; - }); - - return splitShares; -}; - -describe('calculateParticipantSplit', () => { - describe('Edge cases', () => { - it('should return participants unchanged when amount is 0', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.EQUAL, [1n, 1n, 1n]); - - const state: Partial = { - amount: 0n, - participants, - splitType: SplitType.EQUAL, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - expect(result.participants).toEqual(participants); - expect(result.canSplitScreenClosed).toBe(true); - }); - it('should handle empty participants array', () => { - const state: Partial = { - amount: 10000n, - participants: [], - splitType: SplitType.EQUAL, - splitShares: {}, - paidBy: undefined, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - expect(result.participants).toEqual([]); - expect(result.canSplitScreenClosed).toBe(false); - }); - }); - - describe('SplitType.EQUAL', () => { - it('should split amount equally among all participants', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.EQUAL, [1n, 1n, 1n]); - - const state: Partial = { - amount: 30000n, - participants, - splitType: SplitType.EQUAL, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - // Each person should owe $100 (10000n), but payer gets the difference - expect(result.participants[0]?.amount).toBe(20000n); // Payer: -10000n + 30000n = 20000n - expect(result.participants[1]?.amount).toBe(-10000n); // Owes $100 - expect(result.participants[2]?.amount).toBe(-10000n); // Owes $100 - expect(result.canSplitScreenClosed).toBe(true); - }); - - it('should exclude participants with 0 share from equal split', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.EQUAL, [1n, 1n, 0n]); - - const state: Partial = { - amount: 20000n, - participants, - splitType: SplitType.EQUAL, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - // Only first two participants split the cost - expect(result.participants[0]?.amount).toBe(10000n); // Payer: -10000n + 20000n = 10000n - expect(result.participants[1]?.amount).toBe(-10000n); // Owes $100 - expect(result.participants[2]?.amount).toBe(0n); // Excluded from split - }); - - it('should allow one non-payer to owe the full amount', () => { - const participants = createParticipants([user1, user2]); - const splitShares = createSplitShares(participants, SplitType.EQUAL, [0n, 1n]); - - const state: Partial = { - amount: 10000n, - participants, - splitType: SplitType.EQUAL, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - expect(result.participants[0]?.amount).toBe(10000n); - expect(result.participants[1]?.amount).toBe(-10000n); - expect(result.canSplitScreenClosed).toBe(true); - }); - - it('should mark a multi-person self-only split as incomplete', () => { - const participants = createParticipants([user1, user2]); - const splitShares = createSplitShares(participants, SplitType.EQUAL, [1n, 0n]); - - const state: Partial = { - amount: 10000n, - participants, - splitType: SplitType.EQUAL, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - expect(result.participants[0]?.amount).toBe(0n); - expect(result.participants[1]?.amount).toBe(0n); - expect(result.canSplitScreenClosed).toBe(false); - }); - - it('should mark equal split incomplete when every share is disabled', () => { - const participants = createParticipants([user1, user2]); - const splitShares = createSplitShares(participants, SplitType.EQUAL, [0n, 0n]); - - const state: Partial = { - amount: 10000n, - participants, - splitType: SplitType.EQUAL, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - expect(result.canSplitScreenClosed).toBe(false); - }); - - it('should handle uneven division with penny adjustment', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.EQUAL, [1n, 1n, 1n]); - - const state: Partial = { - amount: 10001n, - participants, - splitType: SplitType.EQUAL, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - // Should distribute the extra penny - const totalOwed = result.participants.reduce((sum, p) => sum + (p.amount ?? 0n), 0n); - expect(totalOwed).toBe(0n); // Total should always balance // Payer should get the remainder - expect(result.participants[0]?.amount).toBeGreaterThanOrEqual(6667n); - }); - - it('should handle single participant as payer', () => { - const participants = createParticipants([user1]); - const splitShares = createSplitShares(participants, SplitType.EQUAL, [1n]); - - const state: Partial = { - amount: 10000n, - participants, - splitType: SplitType.EQUAL, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - expect(result.participants[0]?.amount).toBe(0n); // Payer pays and owes nothing - expect(result.canSplitScreenClosed).toBe(true); - }); - }); - - describe('SplitType.PERCENTAGE', () => { - it('should split amount by percentage', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.PERCENTAGE, [ - 5000n /* Lines 153-154 omitted */, - 3000n /* Lines 154-155 omitted */, - 2000n /* Lines 155-156 omitted */, - ]); - - const state: Partial = { - amount: 10000n, - participants, - splitType: SplitType.PERCENTAGE, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - expect(result.participants[0]?.amount).toBe(5000n); // Payer: -5000n + 10000n = 5000n - expect(result.participants[1]?.amount).toBe(-3000n); // Owes 30% - expect(result.participants[2]?.amount).toBe(-2000n); // Owes 20% - expect(result.canSplitScreenClosed).toBe(true); - }); - - it('should mark incomplete when percentages do not sum to 100%', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.PERCENTAGE, [ - 4000n /* Lines 175-176 omitted */, - 3000n /* Lines 176-177 omitted */, - 2000n /* Lines 177-178 omitted */, - ]); - - const state: Partial = { - amount: 10000n, - participants, - splitType: SplitType.PERCENTAGE, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - expect(result.canSplitScreenClosed).toBe(false); - }); - - it('should handle 0% share participants', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.PERCENTAGE, [ - 7000n /* Lines 194-195 omitted */, - 3000n /* Lines 195-196 omitted */, - 0n /* Lines 196-197 omitted */, - ]); - - const state: Partial = { - amount: 10000n, - participants, - splitType: SplitType.PERCENTAGE, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - expect(result.participants[0]?.amount).toBe(3000n); // Payer: -7000n + 10000n - expect(result.participants[1]?.amount).toBe(-3000n); - expect(result.participants[2]?.amount).toBe(0n); // No share - }); - }); - - describe('SplitType.SHARE', () => { - it('should split amount by shares', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.SHARE, [ - 200n /* Lines 217-218 omitted */, - 100n /* Lines 218-219 omitted */, - 100n /* Lines 219-220 omitted */, - ]); - - const state: Partial = { - amount: 12000n, - participants, - splitType: SplitType.SHARE, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - expect(result.participants[0]?.amount).toBe(6000n); // Payer: -6000n + 12000n = 6000n (50%) - expect(result.participants[1]?.amount).toBe(-3000n); // 25% - expect(result.participants[2]?.amount).toBe(-3000n); // 25% - expect(result.canSplitScreenClosed).toBe(true); - }); - - it('should handle participants with 0 shares', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.SHARE, [ - 300n /* Lines 239-240 omitted */, - 100n /* Lines 240-241 omitted */, - 0n /* Lines 241-242 omitted */, - ]); - - const state: Partial = { - amount: 8000n, - participants, - splitType: SplitType.SHARE, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - expect(result.participants[0]?.amount).toBe(2000n); // Payer: -6000n + 8000n (75%) - expect(result.participants[1]?.amount).toBe(-2000n); // 25% - expect(result.participants[2]?.amount).toBe(0n); // No shares - }); - - it('should mark incomplete when no shares are assigned', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.SHARE, [0n, 0n, 0n]); - - const state: Partial = { - amount: 10000n, - participants, - splitType: SplitType.SHARE, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - expect(result.canSplitScreenClosed).toBe(false); - }); - }); - - describe('SplitType.EXACT', () => { - it('should assign exact amounts to participants', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.EXACT, [ - 4000n /* Lines 277-278 omitted */, - 3000n /* Lines 278-279 omitted */, - 3000n /* Lines 279-280 omitted */, - ]); - - const state: Partial = { - amount: 10000n, - participants, - splitType: SplitType.EXACT, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - expect(result.participants[0]?.amount).toBe(6000n); // Payer: -4000n + 10000n - expect(result.participants[1]?.amount).toBe(-3000n); - expect(result.participants[2]?.amount).toBe(-3000n); - expect(result.canSplitScreenClosed).toBe(true); - }); - - it('should mark incomplete when exact amounts do not sum to total', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.EXACT, [ - 4000n /* Lines 299-300 omitted */, - 3000n /* Lines 300-301 omitted */, - 2000n /* Lines 301-302 omitted */, - ]); - - const state: Partial = { - amount: 10000n, - participants, - splitType: SplitType.EXACT, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - expect(result.canSplitScreenClosed).toBe(false); - }); - - it('should handle undefined exact amounts as 0', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.EXACT, [ - 10000n /* Lines 318-319 omitted */, - 0n /* Lines 319-320 omitted */, - 0n /* Lines 320-321 omitted */, - ]); - - const state: Partial = { - amount: 10000n, - participants, - splitType: SplitType.EXACT, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - expect(result.participants[0]?.amount).toBe(0n); // Payer: -10000n + 10000n - expect(result.participants[1]?.amount).toBe(0n); - expect(result.participants[2]?.amount).toBe(0n); - }); - }); - - describe('SplitType.ADJUSTMENT', () => { - it('should distribute remaining amount equally after adjustments', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.ADJUSTMENT, [ - 1000n /* Lines 341-342 omitted */, - -500n /* Lines 342-343 omitted */, - 0n /* Lines 343-344 omitted */, - ]); - - const state: Partial = { - amount: 15000n, - participants, - splitType: SplitType.ADJUSTMENT, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - // Remaining after adjustments: 15000n - 1000n - (-500n) - 0n = 14500n - // Split equally: 14500n / 3 = ~4833n each - const baseShare = (15000n - 1000n - -500n - 0n) / 3n; // 4833n - - expect(result.participants[0]?.amount).toBe(9166n); // Payer adjustment (adjusted for rounding) - expect(result.participants[1]?.amount).toBe(-(baseShare - 500n)); - expect(result.participants[2]?.amount).toBe(-baseShare); - }); - - it('should mark incomplete when adjustments exceed total amount', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.ADJUSTMENT, [ - 8000n /* Lines 366-367 omitted */, - 5000n /* Lines 367-368 omitted */, - 0n, - ]); - - const state: Partial = { - amount: 10000n, - participants, - splitType: SplitType.ADJUSTMENT, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - expect(result.canSplitScreenClosed).toBe(false); - }); - }); - - describe('Payer scenarios', () => { - it('should handle different payers correctly', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.EQUAL, [1n, 1n, 1n]); - - const state: Partial = { - amount: 30000n, - participants, - splitType: SplitType.EQUAL, - splitShares, - paidBy: user2, - expenseDate: new Date('2024-01-01'), - }; - - // Test with user2 as payer - const result = calculateParticipantSplit(state); - - expect(result.participants[0]?.amount).toBe(-10000n); // Owes - expect(result.participants[1]?.amount).toBe(20000n); // Payer: -10000n + 30000n - expect(result.participants[2]?.amount).toBe(-10000n); // Owes - }); - - it('should handle when payer is not in participants list', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.EQUAL, [1n, 1n, 1n]); - - const externalPayer = createMockUser(4, 'David', 'david@example.com'); - - const state: Partial = { - amount: 30000n, - participants, - splitType: SplitType.EQUAL, - splitShares, - paidBy: externalPayer, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - // All participants should owe their share but total balances to 0 due to penny adjustment - expect(result.participants[0]?.amount).toBe(0n); - expect(result.participants[1]?.amount).toBe(0n); - expect(result.participants[2]?.amount).toBe(0n); - }); - }); - - describe('Balance verification', () => { - it('should always maintain balance (total amounts sum to 0)', () => { - const participants = createParticipants([user1, user2, user3]); - const splitShares = createSplitShares(participants, SplitType.PERCENTAGE, [ - 4000n, // 40% - 3500n, // 35% - 2500n, // 25% - ]); - - const state: Partial = { - amount: 12345n, - participants, - splitType: SplitType.PERCENTAGE, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - const totalAmount = result.participants.reduce((sum, p) => sum + (p.amount ?? 0n), 0n); - - expect(totalAmount).toBe(0n); - }); - - it('should handle penny adjustments correctly', () => { - const participants = createParticipants([user1, user2]); - const splitShares = createSplitShares(participants, SplitType.EQUAL, [1n, 1n]); - - const state: Partial = { - amount: 1n, - participants, - splitType: SplitType.EQUAL, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - const result = calculateParticipantSplit(state); - - // One participant should get the penny - const totalAmount = result.participants.reduce((sum, p) => sum + (p.amount ?? 0n), 0n); - - expect(totalAmount).toBe(0n); - expect(result.participants.some((p) => -1n === p.amount)).toBe(true); - }); - }); -}); - -describe('calculateSplitShareBasedOnAmount', () => { - describe('SplitType.EQUAL', () => { - it('should set equal shares for participants with non-zero amounts', () => { - const participants = createParticipants([user1, user2, user3], [5000n, 5000n, 0n]); - const splitShares: Record> = {}; - participants.forEach((p) => { - splitShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount(10000n, participants, SplitType.EQUAL, splitShares, user1); - - expect(splitShares[user1.id][SplitType.EQUAL]).toBe(1n); - expect(splitShares[user2.id][SplitType.EQUAL]).toBe(1n); - expect(splitShares[user3.id][SplitType.EQUAL]).toBe(0n); // Zero amount - }); - }); - - describe('SplitType.PERCENTAGE', () => { - it('should calculate percentage shares for regular participants', () => { - const participants = createParticipants([user1, user2, user3], [-3000n, -2000n, -5000n]); - const splitShares: Record> = {}; - participants.forEach((p) => { - splitShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount(10000n, participants, SplitType.PERCENTAGE, splitShares); - - expect(splitShares[user1.id][SplitType.PERCENTAGE]).toBe(3000n); // 30% - expect(splitShares[user2.id][SplitType.PERCENTAGE]).toBe(2000n); // 20% - expect(splitShares[user3.id][SplitType.PERCENTAGE]).toBe(5000n); // 50% - }); - - it('should calculate percentage shares when payer is specified', () => { - const participants = createParticipants([user1, user2, user3], [8000n, -3000n, -5000n]); - const splitShares: Record> = {}; - participants.forEach((p) => { - splitShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount( - 10000n, - participants, - SplitType.PERCENTAGE, - splitShares, - user1, - ); - - // User1 is payer: paid 10000n but their amount is 8000n, so they owe 2000n (20%) - expect(splitShares[user1.id][SplitType.PERCENTAGE]).toBe(2000n); // 20% - expect(splitShares[user2.id][SplitType.PERCENTAGE]).toBe(3000n); // 30% - expect(splitShares[user3.id][SplitType.PERCENTAGE]).toBe(5000n); // 50% - }); - - it('should handle zero amount', () => { - const participants = createParticipants([user1, user2, user3], [-3000n, -2000n, -5000n]); - const splitShares: Record> = {}; - participants.forEach((p) => { - splitShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount(0n, participants, SplitType.PERCENTAGE, splitShares); - - expect(splitShares[user1.id][SplitType.PERCENTAGE]).toBe(0n); - expect(splitShares[user2.id][SplitType.PERCENTAGE]).toBe(0n); - expect(splitShares[user3.id][SplitType.PERCENTAGE]).toBe(0n); - }); - }); - - describe('SplitType.SHARE', () => { - it('should calculate share values based on participant amounts', () => { - const participants = createParticipants([user1, user2, user3], [-6000n, -3000n, -3000n]); - const splitShares: Record> = {}; - participants.forEach((p) => { - splitShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount(12000n, participants, SplitType.SHARE, splitShares); - - // Shares should be proportional: 6000:3000:3000 = 2:1:1 - // Multiplied by 100 and normalized - expect(splitShares[user1.id][SplitType.SHARE]).toBe(200n); // 2 shares * 100 - expect(splitShares[user2.id][SplitType.SHARE]).toBe(100n); // 1 share * 100 - expect(splitShares[user3.id][SplitType.SHARE]).toBe(100n); // 1 share * 100 - }); - - it('should handle payer in share calculation', () => { - const participants = createParticipants([user1, user2, user3], [6000n, -3000n, -3000n]); - const splitShares: Record> = {}; - participants.forEach((p) => { - splitShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount(12000n, participants, SplitType.SHARE, splitShares, user1); - - // User1 is payer: paid 12000n but their amount is 6000n, so they owe 6000n - // Shares: 6000:3000:3000 = 2:1:1 - expect(splitShares[user1.id][SplitType.SHARE]).toBe(200n); - expect(splitShares[user2.id][SplitType.SHARE]).toBe(100n); - expect(splitShares[user3.id][SplitType.SHARE]).toBe(100n); - }); - - it('should handle zero amount', () => { - const participants = createParticipants([user1, user2, user3], [-6000n, -3000n, -3000n]); - const splitShares: Record> = {}; - participants.forEach((p) => { - splitShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount(0n, participants, SplitType.SHARE, splitShares); - - expect(splitShares[user1.id][SplitType.SHARE]).toBe(0n); - expect(splitShares[user2.id][SplitType.SHARE]).toBe(0n); - expect(splitShares[user3.id][SplitType.SHARE]).toBe(0n); - }); - }); - - describe('SplitType.EXACT', () => { - it('should set exact amounts for regular participants', () => { - const participants = createParticipants([user1, user2, user3], [-4000n, -3000n, -3000n]); - const splitShares: Record> = {}; - participants.forEach((p) => { - splitShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount(10000n, participants, SplitType.EXACT, splitShares); - - expect(splitShares[user1.id][SplitType.EXACT]).toBe(4000n); - expect(splitShares[user2.id][SplitType.EXACT]).toBe(3000n); - expect(splitShares[user3.id][SplitType.EXACT]).toBe(3000n); - }); - - it('should handle payer in exact calculation', () => { - const participants = createParticipants([user1, user2, user3], [6000n, -3000n, -3000n]); - const splitShares: Record> = {}; - participants.forEach((p) => { - splitShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount(10000n, participants, SplitType.EXACT, splitShares, user1); - - // User1 is payer: paid 10000n but their amount is 6000n, so they owe 4000n - expect(splitShares[user1.id][SplitType.EXACT]).toBe(4000n); - expect(splitShares[user2.id][SplitType.EXACT]).toBe(3000n); - expect(splitShares[user3.id][SplitType.EXACT]).toBe(3000n); - }); - - it('should handle zero amounts', () => { - const participants = createParticipants([user1, user2, user3], [0n, 0n, 0n]); - const splitShares: Record> = {}; - participants.forEach((p) => { - splitShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount(10000n, participants, SplitType.EXACT, splitShares); - - expect(splitShares[user1.id][SplitType.EXACT]).toBe(0n); - expect(splitShares[user2.id][SplitType.EXACT]).toBe(0n); - expect(splitShares[user3.id][SplitType.EXACT]).toBe(0n); - }); - }); - - describe('SplitType.ADJUSTMENT', () => { - it('should handle payer in adjustment calculation', () => { - const participants = createParticipants([user1, user2, user3], [-9500n, -4500n, -5000n]); - const splitShares: Record> = {}; - participants.forEach((p) => { - splitShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount( - 15000n, - participants, - SplitType.ADJUSTMENT, - splitShares, - user1, - ); - - expect(splitShares[user1.id][SplitType.ADJUSTMENT]).toBe(1000n); - expect(splitShares[user2.id][SplitType.ADJUSTMENT]).toBe(0n); - expect(splitShares[user3.id][SplitType.ADJUSTMENT]).toBe(500n); - }); - - it('should handle participants with zero amounts', () => { - const participants = createParticipants([user1, user2, user3], [0n, -5000n, 0n]); - const splitShares: Record> = {}; - participants.forEach((p) => { - splitShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount(5000n, participants, SplitType.ADJUSTMENT, splitShares); - - // Only user2 has non-zero amount, so equal share = 5000n / 1 = 5000n - // User2 owes 5000n vs 5000n = 0n adjustment - expect(splitShares[user1.id][SplitType.ADJUSTMENT]).toBe(-5000n); // 0 - 5000n - expect(splitShares[user2.id][SplitType.ADJUSTMENT]).toBe(0n); // 5000n - 5000n - expect(splitShares[user3.id][SplitType.ADJUSTMENT]).toBe(-5000n); // 0 - 5000n - }); - }); - - describe('Edge cases', () => { - it('should handle empty participants array', () => { - const participants: Participant[] = []; - const splitShares: Record> = {}; - - calculateSplitShareBasedOnAmount(10000n, participants, SplitType.EQUAL, splitShares); - - // Should not throw an error and splitShares should remain empty - expect(Object.keys(splitShares)).toHaveLength(0); - }); - - it('should handle when one participant owes entire amount', () => { - const participants = createParticipants([user1, user2], [10000n, -10000n]); - const splitShares: Record> = {}; - participants.forEach((p) => { - splitShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount(10000n, participants, SplitType.EQUAL, splitShares, user1); - - expect(splitShares[user1.id][SplitType.EQUAL]).toBe(0n); - expect(splitShares[user2.id][SplitType.EQUAL]).toBe(1n); - }); - - it('should handle self-payment (no money flow) scenario', () => { - const participants = createParticipants([user1, user2, user3], [0n, 0n, 0n]); - const splitShares: Record> = {}; - participants.forEach((p) => { - splitShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount(10000n, participants, SplitType.EQUAL, splitShares, user1); - - expect(splitShares[user1.id][SplitType.EQUAL]).toBe(1n); - expect(splitShares[user2.id][SplitType.EQUAL]).toBe(0n); - expect(splitShares[user3.id][SplitType.EQUAL]).toBe(0n); - }); - - it('should handle undefined paidBy', () => { - const participants = createParticipants([user1, user2], [-5000n, -5000n]); - const splitShares: Record> = {}; - participants.forEach((p) => { - splitShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount( - 10000n, - participants, - SplitType.PERCENTAGE, - splitShares, - undefined, - ); - - expect(splitShares[user1.id][SplitType.PERCENTAGE]).toBe(5000n); - expect(splitShares[user2.id][SplitType.PERCENTAGE]).toBe(5000n); - }); - - it('should handle all split types with same data', () => { - const participants = createParticipants([user1, user2], [-6000n, -4000n]); - - Object.values(SplitType).forEach((splitType) => { - if (splitType === SplitType.SETTLEMENT) { - return; - } // Skip settlement as it's not implemented - - const splitShares: Record> = {}; - participants.forEach((p) => { - splitShares[p.id] = initSplitShares(); - }); - - // Should not throw for any split type - expect(() => { - calculateSplitShareBasedOnAmount(10000n, participants, splitType, splitShares); - }).not.toThrow(); - }); - }); - }); -}); - -// Integration tests for function reversibility -describe('Function Reversibility Tests', () => { - describe('calculateParticipantSplit -> calculateSplitShareBasedOnAmount', () => { - it('should properly reverse EQUAL split', () => { - const participants = createParticipants([user1, user2, user3]); - const originalShares = [1n, 1n, 1n]; - const splitShares = createSplitShares(participants, SplitType.EQUAL, originalShares); - - const state: Partial = { - amount: 15000n, - participants, - splitType: SplitType.EQUAL, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - // Apply split calculation - const splitResult = calculateParticipantSplit(state); - - // Reverse the calculation - const reversedShares: Record> = {}; - participants.forEach((p) => { - reversedShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount( - 15000n, - splitResult.participants, - SplitType.EQUAL, - reversedShares, - user1, - ); - - // Check if we got back the original shares - expect(reversedShares[user1.id][SplitType.EQUAL]).toBe(originalShares[0]); - expect(reversedShares[user2.id][SplitType.EQUAL]).toBe(originalShares[1]); - expect(reversedShares[user3.id][SplitType.EQUAL]).toBe(originalShares[2]); - }); - - it('should properly reverse PERCENTAGE split', () => { - const participants = createParticipants([user1, user2, user3]); - const originalShares = [5000n, 3000n, 2000n]; // 50%, 30%, 20% - const splitShares = createSplitShares(participants, SplitType.PERCENTAGE, originalShares); - - const state: Partial = { - amount: 20000n, - participants, - splitType: SplitType.PERCENTAGE, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - // Apply split calculation - const splitResult = calculateParticipantSplit(state); - - // Reverse the calculation - const reversedShares: Record> = {}; - participants.forEach((p) => { - reversedShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount( - 20000n, - splitResult.participants, - SplitType.PERCENTAGE, - reversedShares, - user1, - ); - - // Check if we got back the original percentage shares - expect(reversedShares[user1.id][SplitType.PERCENTAGE]).toBe(originalShares[0]); - expect(reversedShares[user2.id][SplitType.PERCENTAGE]).toBe(originalShares[1]); - expect(reversedShares[user3.id][SplitType.PERCENTAGE]).toBe(originalShares[2]); - }); - - it('should properly reverse SHARE split', () => { - const participants = createParticipants([user1, user2, user3]); - const originalShares = [400n, 200n, 200n]; // 2:1:1 ratio - const splitShares = createSplitShares(participants, SplitType.SHARE, originalShares); - - const state: Partial = { - amount: 16000n, - participants, - splitType: SplitType.SHARE, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - // Apply split calculation - const splitResult = calculateParticipantSplit(state); - - // Reverse the calculation - const reversedShares: Record> = {}; - participants.forEach((p) => { - reversedShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount( - 16000n, - splitResult.participants, - SplitType.SHARE, - reversedShares, - user1, - ); - - // Check if we got back proportional shares (should maintain the 2:1:1 ratio) - const ratio1 = Number(reversedShares[user1.id][SplitType.SHARE]) / Number(originalShares[0]); - const ratio2 = Number(reversedShares[user2.id][SplitType.SHARE]) / Number(originalShares[1]); - const ratio3 = Number(reversedShares[user3.id][SplitType.SHARE]) / Number(originalShares[2]); - - // All ratios should be approximately equal (accounting for rounding) - expect(Math.abs(ratio1 - ratio2)).toBeLessThan(0.1); - expect(Math.abs(ratio2 - ratio3)).toBeLessThan(0.1); - }); - - it('should properly reverse EXACT split', () => { - const participants = createParticipants([user1, user2, user3]); - const originalShares = [6000n, 4000n, 2000n]; - const splitShares = createSplitShares(participants, SplitType.EXACT, originalShares); - - const state: Partial = { - amount: 12000n, - participants, - splitType: SplitType.EXACT, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - // Apply split calculation - const splitResult = calculateParticipantSplit(state); - - // Reverse the calculation - const reversedShares: Record> = {}; - participants.forEach((p) => { - reversedShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount( - 12000n, - splitResult.participants, - SplitType.EXACT, - reversedShares, - user1, - ); - - // Check if we got back the original exact amounts - expect(reversedShares[user1.id][SplitType.EXACT]).toBe(originalShares[0]); - expect(reversedShares[user2.id][SplitType.EXACT]).toBe(originalShares[1]); - }); - - it('should handle ADJUSTMENT split reversal (known to have bugs)', () => { - const participants = createParticipants([user1, user2, user3]); - const originalShares = [1000n, 0n, 500n]; // Various adjustments - const splitShares = createSplitShares(participants, SplitType.ADJUSTMENT, originalShares); - - const state: Partial = { - amount: 12300n, - participants, - splitType: SplitType.ADJUSTMENT, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - // Apply split calculation - const splitResult = calculateParticipantSplit(state); - - // Reverse the calculation - const reversedShares: Record> = {}; - participants.forEach((p) => { - reversedShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount( - 12300n, - splitResult.participants, - SplitType.ADJUSTMENT, - reversedShares, - user1, - ); - - expect(reversedShares[user1.id][SplitType.ADJUSTMENT]).toBe(originalShares[0]); - expect(reversedShares[user2.id][SplitType.ADJUSTMENT]).toBe(originalShares[1]); - expect(reversedShares[user3.id][SplitType.ADJUSTMENT]).toBe(originalShares[2]); - }); - - it('should handle edge case with zero amounts', () => { - const participants = createParticipants([user1, user2, user3]); - const originalShares = [1n, 1n, 0n]; // One participant excluded - const splitShares = createSplitShares(participants, SplitType.EQUAL, originalShares); - - const state: Partial = { - amount: 10000n, - participants, - splitType: SplitType.EQUAL, - splitShares, - paidBy: user1, - expenseDate: new Date('2024-01-01'), - }; - - // Apply split calculation - const splitResult = calculateParticipantSplit(state); - - // Reverse the calculation - const reversedShares: Record> = {}; - participants.forEach((p) => { - reversedShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount( - 10000n, - splitResult.participants, - SplitType.EQUAL, - reversedShares, - user1, - ); - - // Check if we preserved the zero share - expect(reversedShares[user1.id][SplitType.EQUAL]).toBe(originalShares[0]); - expect(reversedShares[user2.id][SplitType.EQUAL]).toBe(originalShares[1]); - expect(reversedShares[user3.id][SplitType.EQUAL]).toBe(originalShares[2]); - }); - - it('should handle external payer scenario', () => { - const participants = createParticipants([user1, user2, user3]); - const originalShares = [1n, 1n, 1n]; - const splitShares = createSplitShares(participants, SplitType.EQUAL, originalShares); - const externalPayer = createMockUser(4, 'External', 'external@example.com'); - - const state: Partial = { - amount: 15000n, - participants, - splitType: SplitType.EQUAL, - splitShares, - paidBy: externalPayer, - expenseDate: new Date('2024-01-01'), - }; - - // Apply split calculation with external payer - const splitResult = calculateParticipantSplit(state); - - // Reverse the calculation - const reversedShares: Record> = {}; - participants.forEach((p) => { - reversedShares[p.id] = initSplitShares(); - }); - - calculateSplitShareBasedOnAmount( - 15000n, - splitResult.participants, - SplitType.EQUAL, - reversedShares, - externalPayer, - ); - - // With external payer, all participants have 0 amounts due to balance adjustment - // So reversed shares should all be 0 - expect(reversedShares[user1.id][SplitType.EQUAL]).toBe(0n); - expect(reversedShares[user2.id][SplitType.EQUAL]).toBe(0n); - expect(reversedShares[user3.id][SplitType.EQUAL]).toBe(0n); - }); - }); -}); - -// Regression test for #658: editing a negative expense must preserve the sign -describe('useAddExpenseStore sign preservation on edit (#658)', () => { - const { toUIString } = getCurrencyHelpers({ locale: 'en-US', currency: 'USD' }); - const { actions } = useAddExpenseStore.getState(); - - beforeEach(() => { - actions.resetState(); - }); - - it('loads a negative expense with the minus sign visible in amountStr', () => { - actions.setAmount(-20000n); - actions.setAmountStr(toUIString(-20000n, true, true)); - - const state = useAddExpenseStore.getState(); - expect(state.amountStr).toBe('-200'); - expect(state.isNegative).toBe(true); - expect(state.amount).toBe(20000n); - }); - - it('preserves the sign when the user edits digits without removing the minus', () => { - actions.setAmount(-20000n); - actions.setAmountStr(toUIString(-20000n, true, true)); - - // Simulate the user changing -200 to -200.01 via the input - actions.setAmountStr('-200.01'); - actions.setAmount(-20001n); - - const state = useAddExpenseStore.getState(); - expect(state.amountStr).toBe('-200.01'); - expect(state.isNegative).toBe(true); - expect(state.amount).toBe(20001n); - }); - - it('flips the sign when the user deletes the minus and edits the value', () => { - actions.setAmount(-20000n); - actions.setAmountStr(toUIString(-20000n, true, true)); - - // Simulate the user deleting the minus and changing the value to 200.01 - actions.setAmountStr('200.01'); - actions.setAmount(20001n); - - const state = useAddExpenseStore.getState(); - expect(state.amountStr).toBe('200.01'); - expect(state.isNegative).toBe(false); - expect(state.amount).toBe(20001n); - }); -}); diff --git a/src/tests/currencyPreferenceStore.test.ts b/src/tests/currencyPreferenceStore.test.ts deleted file mode 100644 index 7213a13ae..000000000 --- a/src/tests/currencyPreferenceStore.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { SHOW_ALL_VALUE, useCurrencyPreferenceStore } from '~/store/currencyPreferenceStore'; - -const resetStore = () => - useCurrencyPreferenceStore.setState({ - recentCurrencies: [], - userDefaultCurrency: null, - groupDefaultCurrencies: {}, - preferences: {}, - }); - -beforeEach(() => { - sessionStorage.clear(); - resetStore(); -}); - -describe('getPreference – entity type isolation', () => { - it('returns userDefaultCurrency for a friend entity even when a group with the same id has a different default currency', () => { - const collidingId = 5; - - // Simulate visiting the Groups page: group id=5 has USD as its default - useCurrencyPreferenceStore.getState().setGroupDefaultCurrency(collidingId, 'USD'); - // User's own currency is EUR - useCurrencyPreferenceStore.getState().setUserDefaultCurrency('EUR'); - - // When rendering a friend balance entry (no entityType), USD must not leak in - const preference = useCurrencyPreferenceStore.getState().getPreference(collidingId); - - expect(preference).toBe('EUR'); - expect(preference).not.toBe('USD'); - }); - - it('returns the group default currency when entityType is "group"', () => { - const groupId = 5; - useCurrencyPreferenceStore.getState().setGroupDefaultCurrency(groupId, 'USD'); - useCurrencyPreferenceStore.getState().setUserDefaultCurrency('EUR'); - - const preference = useCurrencyPreferenceStore.getState().getPreference(groupId, 'group'); - - expect(preference).toBe('USD'); - }); - - it('respects an explicit preference over all defaults for both entity types', () => { - const id = 7; - useCurrencyPreferenceStore.getState().setGroupDefaultCurrency(id, 'USD'); - useCurrencyPreferenceStore.getState().setUserDefaultCurrency('EUR'); - useCurrencyPreferenceStore.getState().setPreference(id, 'GBP'); - - expect(useCurrencyPreferenceStore.getState().getPreference(id)).toBe('GBP'); - expect(useCurrencyPreferenceStore.getState().getPreference(id, 'group')).toBe('GBP'); - }); - - it('falls back to SHOW_ALL when no preference, no group default (for group entity), and no user default', () => { - const preference = useCurrencyPreferenceStore.getState().getPreference(99, 'group'); - expect(preference).toBe(SHOW_ALL_VALUE); - }); - - it('falls back to SHOW_ALL for a friend entity with no preference and no user default', () => { - useCurrencyPreferenceStore.getState().setGroupDefaultCurrency(3, 'USD'); - const preference = useCurrencyPreferenceStore.getState().getPreference(3); - expect(preference).toBe(SHOW_ALL_VALUE); - }); - - it('uses the global (string) key when no entityId is provided', () => { - useCurrencyPreferenceStore.getState().setUserDefaultCurrency('JPY'); - expect(useCurrencyPreferenceStore.getState().getPreference()).toBe('JPY'); - }); -}); diff --git a/src/tests/env.test.ts b/src/tests/env.test.ts deleted file mode 100644 index cc3a6c915..000000000 --- a/src/tests/env.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { parseEnvBoolean } from '../utils/env'; - -describe('parseEnvBoolean', () => { - it.each([ - ['1', true], - ['true', true], - ['TRUE', true], - [' TrUe ', true], - ['0', false], - ['false', false], - ['FALSE', false], - [' FaLsE ', false], - ])('parses %p as %p', (value, expected) => { - expect(parseEnvBoolean(value)).toBe(expected); - }); - - it.each([undefined, '', 'on', 'off', 'yes', '2'])('uses false for invalid value %p', (value) => { - expect(parseEnvBoolean(value)).toBe(false); - }); - - it.each([undefined, '', 'on', 'off', 'yes', '2'])( - 'uses the supplied fallback for invalid value %p', - (value) => { - expect(parseEnvBoolean(value, true)).toBe(true); - }, - ); -}); diff --git a/src/tests/healthz.test.ts b/src/tests/healthz.test.ts deleted file mode 100644 index 3812bed72..000000000 --- a/src/tests/healthz.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; - -import handler from '~/pages/api/healthz'; - -const createMockRes = () => { - const res = {} as NextApiResponse; - res.status = jest.fn().mockReturnValue(res); - res.json = jest.fn().mockReturnValue(res); - res.setHeader = jest.fn().mockReturnValue(res); - return res; -}; - -describe('/api/healthz', () => { - describe('GET requests', () => { - it('responds with 200 ok for GET requests', () => { - const req = { method: 'GET' } as NextApiRequest; - const res = createMockRes(); - - handler(req, res); - - expect(res.status).toHaveBeenCalledWith(200); - expect(res.json).toHaveBeenCalledWith({ status: 'ok' }); - }); - }); - - describe('method validation', () => { - it('rejects non-GET methods', () => { - const req = { method: 'POST' } as NextApiRequest; - const res = createMockRes(); - - handler(req, res); - - expect(res.status).toHaveBeenCalledWith(405); - expect((res.setHeader as jest.Mock).mock.calls).toContainEqual(['Allow', 'GET']); - }); - }); -}); diff --git a/src/tests/integration/authorization.integration.test.ts b/src/tests/integration/authorization.integration.test.ts new file mode 100644 index 000000000..013f0bc3f --- /dev/null +++ b/src/tests/integration/authorization.integration.test.ts @@ -0,0 +1,251 @@ +import { TRPCError } from '@trpc/server'; +import { beforeEach, describe, expect, it } from 'vitest'; + +import { db } from '~/server/db'; +import { expenseParticipants, expenses, groupUsers } from '~/server/db/schema'; +import { SplitType } from '~/server/db/types'; +import { eq } from 'drizzle-orm'; + +import { + addExpense, + balanceBetween, + callerFor, + expenseRow, + makeGroup, + makeUser, + participantsOf, + resetDb, +} from './helpers'; + +const rejectsWith = async (promise: Promise, code?: TRPCError['code']) => { + const error = await promise.then( + () => null, + (e: unknown) => e, + ); + expect(error, 'expected the call to be rejected').toBeInstanceOf(TRPCError); + if (code) { + expect((error as TRPCError).code).toBe(code); + } +}; + +describe('authorization', () => { + beforeEach(resetDb); + + it('rejects unauthenticated queries and mutations', async () => { + const anon = callerFor(null); + await rejectsWith(anon.expense.getBalances(), 'UNAUTHORIZED'); + await rejectsWith(anon.group.create({ name: 'x' }), 'UNAUTHORIZED'); + await rejectsWith(anon.user.getFriends(), 'UNAUTHORIZED'); + await rejectsWith( + anon.expense.addOrEditExpense({ + paidBy: 1, + name: 'x', + category: 'general', + amount: 100n, + groupId: null, + splitType: SplitType.EQUAL, + currency: 'EUR', + participants: [], + }), + 'UNAUTHORIZED', + ); + }); + + it('rejects outsider group reads and membership mutations', async () => { + const owner = await makeUser(); + const member = await makeUser(); + const outsider = await makeUser(); + const group = await makeGroup(owner, [member]); + const out = callerFor(outsider); + + await rejectsWith(out.group.getGroupDetails({ groupId: group.id }), 'FORBIDDEN'); + await rejectsWith(out.group.getGroupTotals({ groupId: group.id }), 'FORBIDDEN'); + await rejectsWith( + out.group.addMembers({ groupId: group.id, userIds: [outsider.id] }), + 'FORBIDDEN', + ); + await rejectsWith(out.group.leaveGroup({ groupId: group.id, userId: member.id }), 'FORBIDDEN'); + await rejectsWith(out.group.toggleArchive({ groupId: group.id }), 'FORBIDDEN'); + await rejectsWith(out.group.delete({ groupId: group.id }), 'FORBIDDEN'); + + const members = await db.select().from(groupUsers).where(eq(groupUsers.groupId, group.id)); + expect(members.map((m) => m.userId).sort((x, y) => x - y)).toEqual( + [owner.id, member.id].sort((x, y) => x - y), + ); + }); + + it('only the creator can remove another member, and only the creator can delete a group', async () => { + const owner = await makeUser(); + const member = await makeUser(); + const other = await makeUser(); + const group = await makeGroup(owner, [member, other]); + + await rejectsWith( + callerFor(member).group.leaveGroup({ groupId: group.id, userId: other.id }), + 'UNAUTHORIZED', + ); + await rejectsWith(callerFor(member).group.delete({ groupId: group.id }), 'UNAUTHORIZED'); + }); + + it('rejects outsider expense reads, edits and deletes without changing accounting', async () => { + const alice = await makeUser(); + const bob = await makeUser(); + const mallory = await makeUser(); + const group = await makeGroup(alice, [bob]); + + const created = await addExpense(callerFor(alice), { + paidBy: alice, + amount: 1000n, + groupId: group.id, + shares: { [alice.id]: 500n, [bob.id]: -500n }, + name: 'Dinner', + }); + const mal = callerFor(mallory); + + await rejectsWith(mal.expense.getExpenseDetails({ expenseId: created.id })); + await rejectsWith( + addExpense(mal, { + paidBy: mallory, + amount: 1n, + groupId: group.id, + shares: { [mallory.id]: 1n, [bob.id]: -1n }, + }), + ); + await rejectsWith( + mal.expense.addOrEditExpense({ + expenseId: created.id, + paidBy: alice.id, + name: 'Hijacked', + category: 'general', + amount: 999_999n, + groupId: group.id, + splitType: SplitType.EQUAL, + currency: 'EUR', + participants: [ + { userId: alice.id, amount: 999_999n }, + { userId: bob.id, amount: -999_999n }, + ], + }), + ); + await rejectsWith(mal.expense.deleteExpense({ expenseId: created.id })); + + const row = await expenseRow(created.id); + expect(row?.name).toBe('Dinner'); + expect(row?.deletedAt).toBeNull(); + expect(await participantsOf(created.id)).toEqual( + [ + { userId: alice.id, amount: 500n }, + { userId: bob.id, amount: -500n }, + ].sort((a, b) => a.userId - b.userId), + ); + expect(await balanceBetween(alice.id, bob.id)).toBe(500n); + const stray = await db + .select() + .from(expenseParticipants) + .where(eq(expenseParticipants.userId, mallory.id)); + expect(stray).toHaveLength(0); + }); + + it('lets a group member who is not a participant read and delete a group expense', async () => { + const alice = await makeUser(); + const bob = await makeUser(); + const carol = await makeUser(); + const group = await makeGroup(alice, [bob, carol]); + const created = await addExpense(callerFor(alice), { + paidBy: alice, + amount: 1000n, + groupId: group.id, + shares: { [alice.id]: 500n, [bob.id]: -500n }, + }); + + const details = await callerFor(carol).expense.getExpenseDetails({ expenseId: created.id }); + expect(details?.id).toBe(created.id); + }); + + it('rejects writes to archived groups', async () => { + const alice = await makeUser(); + const bob = await makeUser(); + const group = await makeGroup(alice, [bob]); + const a = callerFor(alice); + + const created = await addExpense(a, { + paidBy: alice, + amount: 1000n, + groupId: group.id, + shares: { [alice.id]: 500n, [bob.id]: -500n }, + }); + // Settle fully so the group may be archived. + await addExpense(callerFor(bob), { + paidBy: bob, + amount: 500n, + groupId: group.id, + splitType: SplitType.SETTLEMENT, + shares: { [bob.id]: 500n, [alice.id]: -500n }, + }); + await a.group.toggleArchive({ groupId: group.id }); + + await rejectsWith( + addExpense(a, { + paidBy: alice, + amount: 100n, + groupId: group.id, + shares: { [alice.id]: 50n, [bob.id]: -50n }, + }), + 'BAD_REQUEST', + ); + await rejectsWith(a.expense.deleteExpense({ expenseId: created.id }), 'BAD_REQUEST'); + await rejectsWith( + a.expense.addOrEditExpense({ + expenseId: created.id, + paidBy: alice.id, + name: 'Edited', + category: 'general', + amount: 1000n, + groupId: group.id, + splitType: SplitType.EQUAL, + currency: 'EUR', + participants: [ + { userId: alice.id, amount: 500n }, + { userId: bob.id, amount: -500n }, + ], + }), + 'BAD_REQUEST', + ); + expect((await expenseRow(created.id))?.name).not.toBe('Edited'); + }); + + it('applies the same membership, edit and archive guards to linked currency conversions', async () => { + const alice = await makeUser(); + const bob = await makeUser(); + const mallory = await makeUser(); + const group = await makeGroup(alice, [bob]); + const conversion = { + amount: 1000n, + from: 'EUR', + to: 'USD', + rate: 1.1, + senderId: alice.id, + receiverId: bob.id, + groupId: group.id, + }; + + // An outsider cannot create a conversion in someone else's group. + await rejectsWith(callerFor(mallory).expense.addOrEditCurrencyConversion(conversion)); + expect(await db.select().from(expenses)).toHaveLength(0); + + // A member can. + await callerFor(alice).expense.addOrEditCurrencyConversion(conversion); + const [from] = await db.select().from(expenses).where(eq(expenses.currency, 'EUR')); + expect(from).toBeDefined(); + + // An outsider cannot edit it. + await rejectsWith( + callerFor(mallory).expense.addOrEditCurrencyConversion({ + ...conversion, + expenseId: from!.id, + amount: 5n, + }), + ); + expect((await expenseRow(from!.id))?.amount).toBe(1000n); + }); +}); diff --git a/src/tests/integration/balances.integration.test.ts b/src/tests/integration/balances.integration.test.ts new file mode 100644 index 000000000..e502dddb3 --- /dev/null +++ b/src/tests/integration/balances.integration.test.ts @@ -0,0 +1,102 @@ +import { beforeEach, describe, expect, it } from 'vitest'; + +import { SplitType } from '~/server/db/types'; + +import { addExpense, balanceBetween, callerFor, makeGroup, makeUser, resetDb } from './helpers'; + +describe('balance isolation and simplification', () => { + beforeEach(resetDb); + + it('isolates groups and currencies and hides deleted expenses', async () => { + const alice = await makeUser(); + const bob = await makeUser(); + const g1 = await makeGroup(alice, [bob]); + const g2 = await makeGroup(alice, [bob]); + const a = callerFor(alice); + const split = (n: bigint) => ({ [alice.id]: n, [bob.id]: -n }); + + await addExpense(a, { paidBy: alice, amount: 2000n, groupId: g1.id, shares: split(1000n) }); + await addExpense(a, { paidBy: alice, amount: 600n, groupId: g2.id, shares: split(300n) }); + await addExpense(a, { paidBy: alice, amount: 400n, currency: 'USD', shares: split(200n) }); + const doomed = await addExpense(a, { + paidBy: alice, + amount: 10_000n, + groupId: g1.id, + shares: split(5000n), + }); + + expect(await balanceBetween(alice.id, bob.id, { groupId: g1.id, currency: 'EUR' })).toBe(6000n); + + await a.expense.deleteExpense({ expenseId: doomed.id }); + + expect(await balanceBetween(alice.id, bob.id, { groupId: g1.id, currency: 'EUR' })).toBe(1000n); + expect(await balanceBetween(alice.id, bob.id, { groupId: g2.id, currency: 'EUR' })).toBe(300n); + expect(await balanceBetween(alice.id, bob.id, { groupId: null, currency: 'USD' })).toBe(200n); + expect(await balanceBetween(bob.id, alice.id, { groupId: g1.id, currency: 'EUR' })).toBe( + -1000n, + ); + + // GetBalances aggregates per friend and currency across groups. + const { balances } = await a.expense.getBalances(); + const withBob = balances.find((b) => b.friendId === bob.id)!; + const amountOf = (currency: string) => + withBob.currencies.find((c) => c.currency === currency)?.amount; + expect(amountOf('EUR')).toBe(1300n); + expect(amountOf('USD')).toBe(200n); + }); + + it('simplifies a three-member chain while preserving every member net balance', async () => { + const alice = await makeUser(); + const bob = await makeUser(); + const carol = await makeUser(); + const group = await makeGroup(alice, [bob, carol]); + + // Alice paid for Bob (100); Bob paid for Carol (100). + await addExpense(callerFor(alice), { + paidBy: alice, + amount: 100n, + groupId: group.id, + shares: { [alice.id]: 100n, [bob.id]: -100n }, + }); + await addExpense(callerFor(bob), { + paidBy: bob, + amount: 100n, + groupId: group.id, + shares: { [bob.id]: 100n, [carol.id]: -100n }, + }); + await callerFor(alice).group.toggleSimplifyDebts({ groupId: group.id }); + + const details = await callerFor(alice).group.getGroupDetails({ groupId: group.id }); + const balances = details?.groupBalances ?? []; + + // The chain collapses to one edge: Carol owes Alice 100, Bob is out of it. + expect(balances.filter((b) => 0n !== b.amount)).toHaveLength(2); + expect(balances).toEqual( + expect.arrayContaining([ + expect.objectContaining({ userId: alice.id, friendId: carol.id, amount: 100n }), + expect.objectContaining({ userId: carol.id, friendId: alice.id, amount: -100n }), + ]), + ); + + // Every member's net position is preserved (and the whole group nets to zero). + const netOf = (userId: number) => + balances.filter((b) => b.userId === userId).reduce((sum, b) => sum + b.amount, 0n); + expect(netOf(alice.id)).toBe(100n); + expect(netOf(bob.id)).toBe(0n); + expect(netOf(carol.id)).toBe(-100n); + expect(balances.reduce((sum, b) => sum + b.amount, 0n)).toBe(0n); + }); + + it('keeps a plain two-person balance symmetric', async () => { + const alice = await makeUser(); + const bob = await makeUser(); + await addExpense(callerFor(alice), { + paidBy: alice, + amount: 1234n, + splitType: SplitType.EXACT, + shares: { [alice.id]: 700n, [bob.id]: -700n }, + }); + expect(await balanceBetween(alice.id, bob.id)).toBe(700n); + expect(await balanceBetween(bob.id, alice.id)).toBe(-700n); + }); +}); diff --git a/src/tests/integration/expense.integration.test.ts b/src/tests/integration/expense.integration.test.ts new file mode 100644 index 000000000..b0e504504 --- /dev/null +++ b/src/tests/integration/expense.integration.test.ts @@ -0,0 +1,174 @@ +import { beforeEach, describe, expect, it } from 'vitest'; + +import { SplitType } from '~/server/db/types'; + +import { + addExpense, + balanceBetween, + callerFor, + expenseRow, + makeGroup, + makeUser, + participantsOf, + resetDb, +} from './helpers'; + +describe('expense lifecycle', () => { + beforeEach(resetDb); + + it.each([false, true])( + 'creates, edits and soft-deletes an expense with exact bidirectional balances (group: %s)', + async (inGroup) => { + const alice = await makeUser(); + const bob = await makeUser(); + const groupId = inGroup ? (await makeGroup(alice, [bob])).id : null; + const a = callerFor(alice); + + const created = await addExpense(a, { + paidBy: alice, + amount: 1000n, + groupId, + name: 'Dinner', + shares: { [alice.id]: 500n, [bob.id]: -500n }, + }); + expect(await balanceBetween(alice.id, bob.id)).toBe(500n); + expect(await balanceBetween(bob.id, alice.id)).toBe(-500n); + + await a.expense.addOrEditExpense({ + expenseId: created.id, + paidBy: alice.id, + name: 'Dinner + drinks', + category: 'food', + amount: 1200n, + groupId, + splitType: SplitType.EQUAL, + currency: 'EUR', + participants: [ + { userId: alice.id, amount: 600n }, + { userId: bob.id, amount: -600n }, + ], + }); + expect((await expenseRow(created.id))?.name).toBe('Dinner + drinks'); + expect((await expenseRow(created.id))?.updatedBy).toBe(alice.id); + expect(await balanceBetween(alice.id, bob.id)).toBe(600n); + expect(await balanceBetween(bob.id, alice.id)).toBe(-600n); + + await callerFor(bob).expense.deleteExpense({ expenseId: created.id }); + const deleted = await expenseRow(created.id); + expect(deleted?.deletedAt).toBeInstanceOf(Date); + expect(deleted?.deletedBy).toBe(bob.id); + expect(await balanceBetween(alice.id, bob.id)).toBe(0n); + expect(await balanceBetween(bob.id, alice.id)).toBe(0n); + }, + ); + + it('preserves the original expense if an edit transaction fails', async () => { + const alice = await makeUser(); + const bob = await makeUser(); + const a = callerFor(alice); + const created = await addExpense(a, { + paidBy: alice, + amount: 1000n, + name: 'Original', + shares: { [alice.id]: 500n, [bob.id]: -500n }, + }); + + // The last statement of the edit batch references a user that does not exist, so the + // Foreign key fails. Atomicity means none of the earlier statements may stay applied. + const missingUserId = 999_999; + await expect( + a.expense.addOrEditExpense({ + expenseId: created.id, + paidBy: alice.id, + name: 'Should not persist', + category: 'food', + amount: 5000n, + groupId: null, + splitType: SplitType.EQUAL, + currency: 'EUR', + participants: [ + { userId: alice.id, amount: 2500n }, + { userId: missingUserId, amount: -2500n }, + ], + }), + ).rejects.toThrow(); + + const row = await expenseRow(created.id); + expect(row?.name).toBe('Original'); + expect(row?.amount).toBe(1000n); + expect(await participantsOf(created.id)).toEqual( + [ + { userId: alice.id, amount: 500n }, + { userId: bob.id, amount: -500n }, + ].sort((x, y) => x.userId - y.userId), + ); + expect(await balanceBetween(alice.id, bob.id)).toBe(500n); + }); + + it('does not create a half-written expense when creation fails', async () => { + const alice = await makeUser(); + await expect( + callerFor(alice).expense.addOrEditExpense({ + paidBy: alice.id, + name: 'Broken', + category: 'food', + amount: 100n, + groupId: null, + splitType: SplitType.EQUAL, + currency: 'EUR', + participants: [ + { userId: alice.id, amount: 50n }, + { userId: 999_999, amount: -50n }, + ], + }), + ).rejects.toThrow(); + + const { expenses } = await import('~/server/db/schema'); + const { db } = await import('~/server/db'); + expect(await db.select().from(expenses)).toHaveLength(0); + }); +}); + +describe('settlement accounting', () => { + beforeEach(resetDb); + + it.each([false, true])( + 'partially then fully settles a debt, editing and deleting the settlement (group: %s)', + async (inGroup) => { + const alice = await makeUser(); + const bob = await makeUser(); + const groupId = inGroup ? (await makeGroup(alice, [bob])).id : null; + + // Bob owes Alice 500. + await addExpense(callerFor(alice), { + paidBy: alice, + amount: 1000n, + groupId, + shares: { [alice.id]: 500n, [bob.id]: -500n }, + }); + expect(await balanceBetween(bob.id, alice.id)).toBe(-500n); + + const settle = (amount: bigint) => + addExpense(callerFor(bob), { + paidBy: bob, + amount, + groupId, + splitType: SplitType.SETTLEMENT, + name: 'Settle up', + shares: { [bob.id]: amount, [alice.id]: -amount }, + }); + + const partial = await settle(200n); + expect(await balanceBetween(bob.id, alice.id)).toBe(-300n); + expect(await balanceBetween(alice.id, bob.id)).toBe(300n); + + await settle(300n); + expect(await balanceBetween(bob.id, alice.id)).toBe(0n); + expect(await balanceBetween(alice.id, bob.id)).toBe(0n); + + // Undo the partial settlement: the debt comes back by exactly that amount. + await callerFor(alice).expense.deleteExpense({ expenseId: partial.id }); + expect(await balanceBetween(alice.id, bob.id)).toBe(200n); + }, + ); +}); diff --git a/src/tests/integration/helpers.ts b/src/tests/integration/helpers.ts new file mode 100644 index 000000000..e1a4513be --- /dev/null +++ b/src/tests/integration/helpers.ts @@ -0,0 +1,148 @@ +import { env } from 'cloudflare:workers'; +import { and, eq } from 'drizzle-orm'; + +import { appRouter } from '~/server/api/root'; +import type { Session } from '~/server/auth'; +import { db } from '~/server/db'; +import { + balanceView, + expenseParticipants, + expenses, + groupUsers, + groups, + users, +} from '~/server/db/schema'; +import { SplitType } from '~/server/db/types'; + +export type TestUser = typeof users.$inferSelect; + +/** Delete every row from every app table (not the migration bookkeeping tables). */ +export const resetDb = async () => { + const { results } = await (env as unknown as { DB: D1Database }).DB.prepare( + `SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_cf_%' AND name != 'd1_migrations'`, + ).all<{ name: string }>(); + await (env as unknown as { DB: D1Database }).DB.batch([ + (env as unknown as { DB: D1Database }).DB.prepare('PRAGMA defer_foreign_keys = on'), + ...results.map(({ name }) => + (env as unknown as { DB: D1Database }).DB.prepare(`DELETE FROM "${name}"`), + ), + ]); +}; + +let counter = 0; + +export const makeUser = async (overrides: Partial = {}) => { + counter += 1; + const [user] = await db + .insert(users) + .values({ name: `User ${counter}`, email: `user${counter}@example.test`, ...overrides }) + .returning(); + return user!; +}; + +export const makeGroup = async (owner: TestUser, members: TestUser[] = [], name = 'Trip') => { + counter += 1; + const [group] = await db + .insert(groups) + .values({ + name: `${name} ${counter}`, + publicId: `pub-${counter}-${crypto.randomUUID()}`, + userId: owner.id, + }) + .returning(); + const everyone = [owner, ...members.filter((m) => m.id !== owner.id)]; + await db.insert(groupUsers).values(everyone.map((u) => ({ groupId: group!.id, userId: u.id }))); + return group!; +}; + +export const sessionFor = (user: TestUser): Session => ({ + user: { + id: user.id, + name: user.name, + email: user.email, + image: user.image, + currency: user.currency, + defaultCurrency: user.defaultCurrency, + obapiProviderId: user.obapiProviderId, + bankingId: user.bankingId, + preferredLanguage: user.preferredLanguage, + hiddenFriendIds: user.hiddenFriendIds, + }, + expires: new Date(Date.now() + 86_400_000).toISOString(), +}); + +/** TRPC caller bound to a user's session, or anonymous when `user` is null. */ +// oxlint-disable-next-line typescript/no-redundant-type-constituents +export const callerFor = (user: TestUser | null) => + appRouter.createCaller({ session: user ? sessionFor(user) : null, db }); + +// --------------------------------------------------------------------------------------------- +// Expense helpers +// --------------------------------------------------------------------------------------------- +type Caller = ReturnType; +type ExpenseInput = Parameters[0]; +type SingleExpenseInput = Extract; + +/** + * Create an expense through the real tRPC procedure. + * `shares` maps userId -> signed amount. The payer is positive, everyone else negative, and the + * amounts sum to zero (the convention the app and BalanceView use). + */ +export const addExpense = async ( + caller: Caller, + opts: { + paidBy: TestUser; + shares: Record; + amount: bigint; + groupId?: number | null; + currency?: string; + splitType?: SingleExpenseInput['splitType']; + name?: string; + cronExpression?: string; + }, +) => { + const [created] = await caller.expense.addOrEditExpense({ + paidBy: opts.paidBy.id, + name: opts.name ?? 'Expense', + category: 'general', + amount: opts.amount, + groupId: opts.groupId ?? null, + splitType: opts.splitType ?? SplitType.EQUAL, + currency: opts.currency ?? 'EUR', + participants: Object.entries(opts.shares).map(([userId, amount]) => ({ + userId: Number(userId), + amount, + })), + cronExpression: opts.cronExpression, + }); + return created!; +}; + +/** Net amount `userId` is owed by `friendId` (positive = friend owes user), summed over rows. */ +export const balanceBetween = async ( + userId: number, + friendId: number, + where: { groupId?: number | null; currency?: string } = {}, +) => { + const rows = await db + .select() + .from(balanceView) + .where( + and( + eq(balanceView.userId, userId), + eq(balanceView.friendId, friendId), + where.currency ? eq(balanceView.currency, where.currency) : undefined, + ), + ); + return rows + .filter((r) => (where.groupId === undefined ? true : r.groupId === where.groupId)) + .reduce((sum, r) => sum + r.amount, 0n); +}; + +export const participantsOf = async (expenseId: string) => + (await db.select().from(expenseParticipants).where(eq(expenseParticipants.expenseId, expenseId))) + .map((p) => ({ userId: p.userId, amount: p.amount })) + .sort((a, b) => a.userId - b.userId); + +export const expenseRow = async (expenseId: string) => + (await db.select().from(expenses).where(eq(expenses.id, expenseId)))[0]; diff --git a/src/tests/integration/queries.integration.test.ts b/src/tests/integration/queries.integration.test.ts new file mode 100644 index 000000000..179abf182 --- /dev/null +++ b/src/tests/integration/queries.integration.test.ts @@ -0,0 +1,138 @@ +import { eq } from 'drizzle-orm'; +import { beforeEach, describe, expect, it } from 'vitest'; + +import { getCompleteGroupDetails } from '~/server/api/services/splitService'; +import { db } from '~/server/db'; +import { users } from '~/server/db/schema'; +import { SplitType } from '~/server/db/types'; + +import { addExpense, callerFor, makeGroup, makeUser, resetDb } from './helpers'; + +/** + * Regression tests for the Drizzle/D1 query layer: every one of these ran real SQL against D1. + * `getFriend` and `getCompleteGroupDetails` once failed in production with + * "no such column: User.id" (correlated subquery against an aliased table). + */ +describe('query layer on D1', () => { + beforeEach(resetDb); + + it('getFriend returns a friend only when a balance relationship exists', async () => { + const alice = await makeUser(); + const bob = await makeUser(); + const stranger = await makeUser(); + await addExpense(callerFor(alice), { + paidBy: alice, + amount: 1000n, + shares: { [alice.id]: 500n, [bob.id]: -500n }, + }); + + const friend = await callerFor(alice).user.getFriend({ friendId: bob.id }); + expect(friend?.id).toBe(bob.id); + expect((await callerFor(bob).user.getFriend({ friendId: alice.id }))?.id).toBe(alice.id); + expect(await callerFor(alice).user.getFriend({ friendId: stranger.id })).toBeFalsy(); + }); + + it('getCompleteGroupDetails only returns the groups the user belongs to', async () => { + const alice = await makeUser(); + const bob = await makeUser(); + const carol = await makeUser(); + const shared = await makeGroup(alice, [bob], 'Shared'); + await makeGroup(carol, [], 'Private'); + await addExpense(callerFor(alice), { + paidBy: alice, + amount: 1000n, + groupId: shared.id, + shares: { [alice.id]: 500n, [bob.id]: -500n }, + }); + + const aliceGroups = await getCompleteGroupDetails(alice.id); + expect(aliceGroups.map((g) => g.id)).toEqual([shared.id]); + expect(aliceGroups[0]!.groupBalances.some((b) => 0n !== b.amount)).toBe(true); + expect((await getCompleteGroupDetails(carol.id)).map((g) => g.name)[0]).toContain('Private'); + expect(await getCompleteGroupDetails((await makeUser()).id)).toEqual([]); + }); + + it('deleteFriend refuses while a balance is outstanding, then hides the friend once', async () => { + const alice = await makeUser(); + const bob = await makeUser(); + await addExpense(callerFor(alice), { + paidBy: alice, + amount: 1000n, + shares: { [alice.id]: 500n, [bob.id]: -500n }, + }); + await expect(callerFor(alice).user.deleteFriend({ friendId: bob.id })).rejects.toThrow( + /outstanding balances/, + ); + + await addExpense(callerFor(bob), { + paidBy: bob, + amount: 500n, + splitType: SplitType.SETTLEMENT, + shares: { [bob.id]: 500n, [alice.id]: -500n }, + }); + await callerFor(alice).user.deleteFriend({ friendId: bob.id }); + await callerFor(alice).user.deleteFriend({ friendId: bob.id }); + + const [row] = await db.select().from(users).where(eq(users.id, alice.id)); + expect(row!.hiddenFriendIds).toEqual([bob.id]); + }); + + it('a new shared expense un-hides the friend on both sides (trigger)', async () => { + const alice = await makeUser(); + const bob = await makeUser(); + const carol = await makeUser(); + await db + .update(users) + .set({ hiddenFriendIds: [bob.id, carol.id] }) + .where(eq(users.id, alice.id)); + await db + .update(users) + .set({ hiddenFriendIds: [alice.id] }) + .where(eq(users.id, bob.id)); + + await addExpense(callerFor(bob), { + paidBy: bob, + amount: 1000n, + shares: { [bob.id]: 500n, [alice.id]: -500n }, + }); + + const [a] = await db.select().from(users).where(eq(users.id, alice.id)); + const [b] = await db.select().from(users).where(eq(users.id, bob.id)); + expect(a!.hiddenFriendIds).toEqual([carol.id]); + expect(b!.hiddenFriendIds).toEqual([]); + }); + + it('every read procedure runs valid SQL against a populated database', async () => { + const alice = await makeUser(); + const bob = await makeUser(); + const group = await makeGroup(alice, [bob]); + const a = callerFor(alice); + const expense = await addExpense(a, { + paidBy: alice, + amount: 1000n, + groupId: group.id, + shares: { [alice.id]: 500n, [bob.id]: -500n }, + cronExpression: '0 9 * * *', + }); + + await expect(a.user.me()).resolves.toBeDefined(); + await expect(a.user.getFriends()).resolves.toBeDefined(); + await expect(a.user.getOwnExpenses()).resolves.toHaveLength(1); + await expect(a.user.getBalancesWithFriend({ friendId: bob.id })).resolves.toBeDefined(); + await expect(a.user.getUserDetails({ userId: bob.id })).resolves.toBeDefined(); + await expect(a.user.downloadData()).resolves.toBeDefined(); + await expect(a.expense.getBalances()).resolves.toBeDefined(); + await expect(a.expense.getCumulatedBalances()).resolves.toBeDefined(); + await expect(a.expense.getExpensesWithFriend({ friendId: bob.id })).resolves.toBeDefined(); + await expect(a.expense.getGroupExpenses({ groupId: group.id })).resolves.toHaveLength(1); + await expect(a.expense.getExpenseDetails({ expenseId: expense.id })).resolves.toMatchObject({ + id: expense.id, + }); + await expect(a.expense.getAllExpenses()).resolves.toBeDefined(); + await expect(a.expense.getRecurringExpenses()).resolves.toHaveLength(1); + await expect(a.group.getAllGroups()).resolves.toHaveLength(1); + await expect(a.group.getAllGroupsWithBalances()).resolves.toBeDefined(); + await expect(a.group.getGroupDetails({ groupId: group.id })).resolves.toBeDefined(); + await expect(a.group.getGroupTotals({ groupId: group.id })).resolves.toBeDefined(); + }); +}); diff --git a/src/tests/integration/recurrence.integration.test.ts b/src/tests/integration/recurrence.integration.test.ts new file mode 100644 index 000000000..28777df15 --- /dev/null +++ b/src/tests/integration/recurrence.integration.test.ts @@ -0,0 +1,135 @@ +import { eq } from 'drizzle-orm'; +import { beforeEach, describe, expect, it } from 'vitest'; + +import { getDueJobs } from '~/server/api/services/scheduleService'; +import { runRecurringJob } from '~/server/api/services/splitService'; +import { db } from '~/server/db'; +import { expenseRecurrences, expenses, jobs } from '~/server/db/schema'; + +import { + addExpense, + balanceBetween, + callerFor, + expenseRow, + makeUser, + participantsOf, + resetDb, +} from './helpers'; + +const copiesOf = async (recurrenceId: number, templateId: string) => + (await db.select().from(expenses).where(eq(expenses.recurrenceId, recurrenceId))).filter( + (e) => e.id !== templateId, + ); + +describe('recurrence lifecycle', () => { + beforeEach(resetDb); + + const setup = async () => { + const alice = await makeUser(); + const bob = await makeUser(); + const a = callerFor(alice); + const template = await addExpense(a, { + paidBy: alice, + amount: 1000n, + name: 'Rent', + shares: { [alice.id]: 500n, [bob.id]: -500n }, + cronExpression: '0 9 * * *', + }); + const recurrenceId = (await expenseRow(template.id))!.recurrenceId!; + const [job] = await db.select().from(jobs); + return { alice, bob, a, template, recurrenceId, job: job! }; + }; + + it('creates a job linked to the template expense', async () => { + const { template, job, recurrenceId } = await setup(); + expect(recurrenceId).toBeGreaterThan(0); + expect(job.jobname).toBe(`expense_recurring_${template.id}`); + expect(job.command).toContain(template.id); + expect(job.active).toBe(true); + expect(job.nextRunAt!.getTime()).toBeGreaterThan(Date.now()); + const [recurrence] = await db.select().from(expenseRecurrences); + expect(recurrence?.jobId).toBe(job.jobid); + }); + + it('duplicates participants and balances once per slot, even if the step is retried', async () => { + const { alice, bob, template, recurrenceId, job } = await setup(); + const slot1 = job.nextRunAt!; + const now1 = new Date(slot1.getTime() + 60_000); + + expect((await getDueJobs(new Date(slot1.getTime() - 1))).map((j) => j.jobid)).not.toContain( + job.jobid, + ); + expect((await getDueJobs(now1)).map((j) => j.jobid)).toContain(job.jobid); + + const first = await runRecurringJob(job.jobid, now1); + expect(first.skipped).toBe(false); + let copies = await copiesOf(recurrenceId, template.id); + expect(copies).toHaveLength(1); + expect(await participantsOf(copies[0]!.id)).toEqual(await participantsOf(template.id)); + expect(await balanceBetween(alice.id, bob.id)).toBe(1000n); // Template + one copy + + const [afterFirst] = await db.select().from(jobs); + expect(afterFirst!.nextRunAt!.getTime()).toBeGreaterThan(slot1.getTime()); + expect(afterFirst!.lastRunAt).toBeInstanceOf(Date); + expect((await db.select().from(expenseRecurrences))[0]?.notified).toBe(false); + + // Same slot again: nothing is due, nothing is duplicated. + expect((await runRecurringJob(job.jobid, now1)).skipped).toBe(true); + + // A retry after the job row was rolled back to the old slot must not create a second copy. + await db.update(jobs).set({ nextRunAt: slot1 }).where(eq(jobs.jobid, job.jobid)); + await runRecurringJob(job.jobid, now1); + copies = await copiesOf(recurrenceId, template.id); + expect(copies).toHaveLength(1); + expect(await balanceBetween(alice.id, bob.id)).toBe(1000n); + + // The next slot produces a second, distinct copy. + const [afterRetry] = await db.select().from(jobs); + await runRecurringJob(job.jobid, new Date(afterRetry!.nextRunAt!.getTime() + 60_000)); + copies = await copiesOf(recurrenceId, template.id); + expect(copies).toHaveLength(2); + expect(new Set(copies.map((c) => c.id)).size).toBe(2); + expect(await balanceBetween(alice.id, bob.id)).toBe(1500n); + }); + + it('edits the schedule and stops only when the template is deleted', async () => { + const { a, alice, template, recurrenceId, job } = await setup(); + + await a.expense.updateRecurrence({ recurrenceId, cronExpression: '30 18 * * *' }); + const [edited] = await db.select().from(jobs); + expect(edited!.schedule).toBe('30 18 * * *'); + expect(edited!.nextRunAt!.getUTCHours()).toBe(18); + expect(edited!.nextRunAt!.getUTCMinutes()).toBe(30); + + // Deleting a generated copy keeps the recurrence running. + await runRecurringJob(job.jobid, new Date(edited!.nextRunAt!.getTime() + 60_000)); + const [copy] = await copiesOf(recurrenceId, template.id); + await a.expense.deleteExpense({ expenseId: copy!.id }); + expect(await db.select().from(jobs)).toHaveLength(1); + + // Deleting the template stops it. + await a.expense.deleteExpense({ expenseId: template.id }); + expect(await db.select().from(jobs)).toHaveLength(0); + expect(await db.select().from(expenseRecurrences)).toHaveLength(0); + expect((await expenseRow(template.id))?.recurrenceId).toBeNull(); + void alice; + }); + + it('deleteRecurrence removes the job but keeps the expenses', async () => { + const { a, template, recurrenceId } = await setup(); + await a.expense.deleteRecurrence({ recurrenceId }); + expect(await db.select().from(jobs)).toHaveLength(0); + expect((await expenseRow(template.id))?.deletedAt).toBeNull(); + expect((await expenseRow(template.id))?.recurrenceId).toBeNull(); + }); + + it("does not let a stranger change someone else's recurrence", async () => { + const { recurrenceId } = await setup(); + const stranger = await makeUser(); + await expect( + callerFor(stranger).expense.updateRecurrence({ recurrenceId, cronExpression: '0 0 * * *' }), + ).rejects.toThrow(); + await expect(callerFor(stranger).expense.deleteRecurrence({ recurrenceId })).rejects.toThrow(); + expect(await db.select().from(jobs)).toHaveLength(1); + }); +}); diff --git a/src/tests/mock/example_group_balances.json b/src/tests/mock/example_group_balances.json deleted file mode 100644 index f754551b9..000000000 --- a/src/tests/mock/example_group_balances.json +++ /dev/null @@ -1,1490 +0,0 @@ -[ - { - "groupId": 2, - "currency": "NZD", - "userId": 14, - "friendId": 3, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 3, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 14, - "friendId": 1, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 1, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 14, - "friendId": 4, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 4, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 14, - "friendId": 7, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 7, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 14, - "friendId": 9, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 9, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 14, - "friendId": 12, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 12, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 14, - "friendId": 11, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 11, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 14, - "friendId": 13, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 13, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 14, - "friendId": 10, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 10, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 14, - "friendId": 8, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "NZD", - "userId": 8, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 14, - "friendId": 4, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 4, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 6, - "friendId": 3, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 3, - "friendId": 6, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 6, - "friendId": 1, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 14, - "friendId": 7, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 7, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 14, - "friendId": 9, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 9, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 14, - "friendId": 12, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 12, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 14, - "friendId": 3, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 3, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 14, - "friendId": 1, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 1, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 14, - "friendId": 8, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 8, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 14, - "friendId": 11, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 11, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 14, - "friendId": 13, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 13, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 14, - "friendId": 10, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 10, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 1, - "friendId": 6, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 6, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 14, - "friendId": 6, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 6, - "friendId": 8, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "USD", - "userId": 8, - "friendId": 6, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 7, - "friendId": 9, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 9, - "friendId": 7, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 4, - "friendId": 6, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 6, - "friendId": 4, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 4, - "friendId": 7, - "amount": "2700", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 7, - "friendId": 4, - "amount": "-2700", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 4, - "friendId": 9, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 9, - "friendId": 4, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 3, - "friendId": 4, - "amount": "5495", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 4, - "friendId": 3, - "amount": "-5495", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 3, - "friendId": 6, - "amount": "81537", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 6, - "friendId": 3, - "amount": "-81537", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 3, - "friendId": 7, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 7, - "friendId": 3, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 3, - "friendId": 9, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 9, - "friendId": 3, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 10, - "friendId": 3, - "amount": "-12227", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 3, - "friendId": 10, - "amount": "12227", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 10, - "friendId": 6, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 6, - "friendId": 10, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 10, - "friendId": 7, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 7, - "friendId": 10, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 10, - "friendId": 9, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 9, - "friendId": 10, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 14, - "friendId": 3, - "amount": "-78708", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 3, - "friendId": 14, - "amount": "78708", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 14, - "friendId": 4, - "amount": "2600", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 4, - "friendId": 14, - "amount": "-2600", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 14, - "friendId": 6, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 6, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 14, - "friendId": 7, - "amount": "427", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 7, - "friendId": 14, - "amount": "-427", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 14, - "friendId": 9, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 9, - "friendId": 14, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 14, - "friendId": 10, - "amount": "227", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 10, - "friendId": 14, - "amount": "-227", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 1, - "friendId": 4, - "amount": "-2700", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 4, - "friendId": 1, - "amount": "2700", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 1, - "friendId": 6, - "amount": "32517", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 6, - "friendId": 1, - "amount": "-32517", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 1, - "friendId": 7, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 7, - "friendId": 1, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 1, - "friendId": 9, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 9, - "friendId": 1, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 1, - "friendId": 14, - "amount": "30417", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 14, - "friendId": 1, - "amount": "-30417", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 13, - "friendId": 1, - "amount": "1000", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 1, - "friendId": 13, - "amount": "-1000", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 13, - "friendId": 3, - "amount": "20738", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 3, - "friendId": 13, - "amount": "-20738", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 13, - "friendId": 4, - "amount": "2400", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 4, - "friendId": 13, - "amount": "-2400", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 13, - "friendId": 6, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 6, - "friendId": 13, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 13, - "friendId": 7, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 7, - "friendId": 13, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 13, - "friendId": 9, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 9, - "friendId": 13, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 13, - "friendId": 14, - "amount": "-13999", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 14, - "friendId": 13, - "amount": "13999", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 10, - "friendId": 1, - "amount": "9400", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 1, - "friendId": 10, - "amount": "-9400", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 10, - "friendId": 4, - "amount": "400", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 4, - "friendId": 10, - "amount": "-400", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 13, - "friendId": 10, - "amount": "-6400", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 10, - "friendId": 13, - "amount": "6400", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 8, - "friendId": 1, - "amount": "-29917", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 1, - "friendId": 8, - "amount": "29917", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 8, - "friendId": 3, - "amount": "46133", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 3, - "friendId": 8, - "amount": "-46133", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 8, - "friendId": 4, - "amount": "-1800", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 4, - "friendId": 8, - "amount": "1800", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 8, - "friendId": 6, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 6, - "friendId": 8, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 8, - "friendId": 7, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 7, - "friendId": 8, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 8, - "friendId": 9, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 9, - "friendId": 8, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 8, - "friendId": 10, - "amount": "-8500", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 10, - "friendId": 8, - "amount": "8500", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 8, - "friendId": 13, - "amount": "15500", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 13, - "friendId": 8, - "amount": "-15500", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 8, - "friendId": 14, - "amount": "-9672", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 14, - "friendId": 8, - "amount": "9672", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 11, - "friendId": 1, - "amount": "-960", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 1, - "friendId": 11, - "amount": "960", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 11, - "friendId": 3, - "amount": "44672", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 3, - "friendId": 11, - "amount": "-44672", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 11, - "friendId": 4, - "amount": "24985", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 4, - "friendId": 11, - "amount": "-24985", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 11, - "friendId": 6, - "amount": "6300", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 6, - "friendId": 11, - "amount": "-6300", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 11, - "friendId": 7, - "amount": "1100", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 7, - "friendId": 11, - "amount": "-1100", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 11, - "friendId": 8, - "amount": "18124", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 8, - "friendId": 11, - "amount": "-18124", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 11, - "friendId": 9, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 9, - "friendId": 11, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 11, - "friendId": 10, - "amount": "1140", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 10, - "friendId": 11, - "amount": "-1140", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 11, - "friendId": 13, - "amount": "-2176", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 13, - "friendId": 11, - "amount": "2176", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 11, - "friendId": 14, - "amount": "24385", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 14, - "friendId": 11, - "amount": "-24385", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 11, - "friendId": 17, - "amount": "9043", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 17, - "friendId": 11, - "amount": "-9043", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "EUR", - "userId": 3, - "friendId": 12, - "amount": "-4637", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "EUR", - "userId": 12, - "friendId": 3, - "amount": "4637", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "EUR", - "userId": 1, - "friendId": 3, - "amount": "-9251", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "EUR", - "userId": 3, - "friendId": 1, - "amount": "9251", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "EUR", - "userId": 1, - "friendId": 12, - "amount": "-12760", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "EUR", - "userId": 12, - "friendId": 1, - "amount": "12760", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 1, - "friendId": 3, - "amount": "5808", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 3, - "friendId": 1, - "amount": "-5808", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 12, - "friendId": 1, - "amount": "-26585", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 1, - "friendId": 12, - "amount": "26585", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 12, - "friendId": 4, - "amount": "2000", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 4, - "friendId": 12, - "amount": "-2000", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 12, - "friendId": 6, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 6, - "friendId": 12, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 12, - "friendId": 7, - "amount": "2500", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 7, - "friendId": 12, - "amount": "-2500", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 12, - "friendId": 8, - "amount": "-7888", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 8, - "friendId": 12, - "amount": "7888", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 12, - "friendId": 9, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 9, - "friendId": 12, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 12, - "friendId": 10, - "amount": "-4067", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 10, - "friendId": 12, - "amount": "4067", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 12, - "friendId": 13, - "amount": "1500", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 13, - "friendId": 12, - "amount": "-1500", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 12, - "friendId": 14, - "amount": "4800", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 14, - "friendId": 12, - "amount": "-4800", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 12, - "friendId": 17, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 17, - "friendId": 12, - "amount": "0", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 12, - "friendId": 3, - "amount": "-9922", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 3, - "friendId": 12, - "amount": "9922", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 12, - "friendId": 11, - "amount": "20277", - "updatedAt": "2025-10-19T06:44:46.676Z" - }, - { - "groupId": 2, - "currency": "PLN", - "userId": 11, - "friendId": 12, - "amount": "-20277", - "updatedAt": "2025-10-19T06:44:46.676Z" - } -] diff --git a/src/tests/number.test.ts b/src/tests/number.test.ts deleted file mode 100644 index 15c14456a..000000000 --- a/src/tests/number.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -// Filepath: /home/wiktor/kod/split-pro/src/utils/number.test.ts -import { currencyConversion, getCurrencyHelpers } from '../utils/numbers'; - -describe('getCurrencyHelpers', () => { - describe('toUIString', () => { - describe('en-US locale', () => { - const locale = 'en-US'; - - describe('USD', () => { - const currency = 'USD'; - const { toUIString } = getCurrencyHelpers({ locale, currency }); - - it.each([ - [12345n, '$123.45'], - [-12345n, '$123.45'], - [-50n, '$0.5'], - [-0n, '$0'], - [99999999999999999999999999999n, '$999,999,999,999,999,999,999,999,999.99'], - [-99999999999999999999999999999n, '$999,999,999,999,999,999,999,999,999.99'], - ])('should format %p as %p ', (value, expected) => { - expect(toUIString(value)).toBe(expected); - }); - - it('should format with thousands separators for large numbers', () => { - const value = 12345678900n; - expect(toUIString(value)).toBe('$123,456,789'); - }); - - it('should insert thousands separators for single ~1k values', () => { - const value = 123400n; - expect(toUIString(value)).toBe('$1,234'); - }); - - it('Should correctly format single digit cent amounts', () => { - const value = 7n; - expect(toUIString(value)).toBe('$0.07'); - }); - - it.each([ - [12345n, '$123.45'], - [-12345n, '-$123.45'], - [-50n, '-$0.5'], - [-0n, '$0'], - [99999999999999999999999999999n, '$999,999,999,999,999,999,999,999,999.99'], - [-99999999999999999999999999999n, '-$999,999,999,999,999,999,999,999,999.99'], - ])('should format %p as %p with signed flag', (value, expected) => { - expect(toUIString(value, true)).toBe(expected); - }); - - it.each([ - [12345n, '123.45'], - [-12345n, '-123.45'], - [-50n, '-0.5'], - [-0n, '0'], - ])('should format %p as %p with signed flag and hideSymbol', (value, expected) => { - expect(toUIString(value, true, true)).toBe(expected); - }); - }); - describe('JPY (no decimals)', () => { - const currency = 'JPY'; - - const { toUIString } = getCurrencyHelpers({ locale, currency }); - - it.each([ - [12345n, '¥12,345'], - [-12345n, '¥12,345'], - [-50n, '¥50'], - [-0n, '¥0'], - ])('should format %p as %p ', (value, expected) => { - expect(toUIString(value)).toBe(expected); - }); - }); - - describe('NZD', () => { - const { toSafeBigInt, toUIString } = getCurrencyHelpers({ - locale: 'en-NZ', - currency: 'NZD', - }); - - it('preserves one cent when parsing and displaying an amount', () => { - expect(toSafeBigInt('0.01')).toBe(1n); - expect(toUIString(1n, false, true)).toBe('0.01'); - }); - }); - }); - - describe('bg-BG locale', () => { - const locale = 'bg-BG'; - - describe('EUR', () => { - const currency = 'EUR'; - - const { toUIString } = getCurrencyHelpers({ locale, currency }); - - it.each([ - [12345n, '123,45 €'], - [-12345n, '123,45 €'], - [-50n, '0,5 €'], - [-0n, '0 €'], - ])('should format %p as %p ', (value, expected) => { - expect(toUIString(value)).toBe(expected); - }); - - it('should format with thousands separators for large numbers', () => { - expect(toUIString(12345678900n)).toBe('123 456 789 €'); - expect(toUIString(5678900n)).toBe('56 789 €'); - }); - - it('should not insert thousands separators for single ~1k values', () => { - const value = 123400n; - expect(toUIString(value)).toBe('1234 €'); - }); - }); - }); - }); - - describe('toSafeBigInt', () => { - const { toSafeBigInt } = getCurrencyHelpers({ - locale: 'en-US', - currency: 'USD', - }); - - it.each([ - ['123.45', 12345n], - ['0.99', 99n], - ['1000', 100000n], - ['1000.5', 100050n], - ['0', 0n], - ['', 0n], - ['invalid', 0n], - [123.45, 12345n], - [0.99, 99n], - [1000, 100000n], - [1000.5, 100050n], - [NaN, 0n], - [0, 0n], - [12345n, 12345n], - [0n, 0n], - ])('should parse %p to %p ', (input, expected) => { - expect(toSafeBigInt(input)).toBe(expected); - }); - - it.each([ - ['-123.45', -12345n], - ['-0.99', -99n], - ['-100.01', -10001n], - ['-1000', -100000n], - ['-1000.5', -100050n], - - [-123.45, -12345n], - [-0.99, -99n], - [-1000, -100000n], - [-1000.5, -100050n], - ])('should parse %p to %p with signed flag', (input, expected) => { - expect(toSafeBigInt(input, true)).toBe(expected); - }); - - it.each([ - ['123456789012345678901234567890.12', 12345678901234567890123456789012n], - ['0.000000000000000000000000000001', 0n], - ['-123456789012345678901234567890.12', -12345678901234567890123456789012n], - ])('should parse big numbers %p to %p ', (input, expected) => { - expect(toSafeBigInt(input, true)).toBe(expected); - }); - }); - - describe('sanitizeInput', () => { - const { sanitizeInput } = getCurrencyHelpers({ - locale: 'en-US', - currency: 'USD', - }); - - it.each([ - ['123.45', '123.45'], - ['1,234.56', '1234.56'], - ['1,234.56789', '1234.56'], - ['$1,234.56', '1234.56'], - ['$1,234.56789', '1234.56'], - ['-123.45', '123.45'], - ['abc123.45xyz', '123.45'], - ['..123..45..', '.12'], - ['', ''], - ])('should sanitize %p to %p', (input, expected) => { - expect(sanitizeInput(input, false)).toBe(expected); - }); - - it.each([ - ['-123.45', '-123.45'], - ['(-123.45)', '-123.45'], - ['-$1,234.56', '-1234.56'], - ['abc-123.45xyz', '-123.45'], - ['--123.45', '-123.45'], - ])('should sanitize %p to %p with signed flag', (input, expected) => { - expect(sanitizeInput(input, true)).toBe(expected); - }); - }); - - describe('parseToCleanString', () => { - const { parseToCleanString } = getCurrencyHelpers({ - locale: 'en-US', - currency: 'USD', - }); - - it.each([ - ['-200', '-200'], - ['-200.01', '-200.01'], - ['--200', '-200'], - ['-$200.00', '-200.00'], - ])('should keep the minus sign for %p with signed flag', (input, expected) => { - expect(parseToCleanString(input, true)).toBe(expected); - }); - - it.each([ - [-20000n, '-200.00'], - [-12345n, '-123.45'], - [-50n, '-0.50'], - [-0n, '0.00'], - ])('should keep the minus sign for bigint %p with signed flag', (value, expected) => { - expect(parseToCleanString(value, true)).toBe(expected); - }); - - it.each([ - ['-200', '200'], - ['-123.45', '123.45'], - ])('should drop the minus sign for %p without signed flag', (input, expected) => { - expect(parseToCleanString(input)).toBe(expected); - }); - }); -}); - -describe('currencyConversion', () => { - it('handles increasing decimal digit conversions', () => { - const from = 'JPY'; // 0 decimal digits - const to = 'USD'; - const amount = 12345n; // 12345 JPY - const rate = 0.0073; // 1 JPY = 0.0073 USD - - const res = currencyConversion({ from, to, amount, rate }); - - // 12345 JPY * 0.0073 = 90.1185 USD -> rounded to 90.12 USD -> 9012 in bigint - expect(res).toBe(9012n); - }); - - it('handles decreasing decimal digit conversions', () => { - const from = 'USD'; // 2 decimal digits - const to = 'JPY'; - const amount = 9012n; - const rate = 1 / 0.0073; - - const res = currencyConversion({ from, to, amount, rate }); - - expect(res).toBe(12345n); - }); -}); diff --git a/src/tests/readyz.test.ts b/src/tests/readyz.test.ts deleted file mode 100644 index 6ebbef648..000000000 --- a/src/tests/readyz.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type { Prisma } from '~/server/db/types'; -import type { NextApiRequest, NextApiResponse } from 'next'; - -jest.mock('~/server/db', () => ({ - db: { - $executeRaw: jest.fn(), - $queryRaw: jest.fn(), - $transaction: jest.fn(), - }, -})); - -import { db } from '~/server/db'; -import handler from '~/pages/api/readyz'; - -const createMockRes = () => { - const res = {} as NextApiResponse; - res.status = jest.fn().mockReturnValue(res); - res.json = jest.fn().mockReturnValue(res); - res.setHeader = jest.fn().mockReturnValue(res); - return res; -}; - -describe('/api/readyz', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('successful checks', () => { - it('responds with 200 ok when the database is reachable', async () => { - (db.$transaction as jest.Mock).mockResolvedValue([undefined, [{ '?column?': 1 }]]); - const req = { method: 'GET' } as NextApiRequest; - const res = createMockRes(); - - await handler(req, res); - - expect(res.status).toHaveBeenCalledWith(200); - expect(res.json).toHaveBeenCalledWith({ status: 'ok' }); - }); - - it('scopes a 3s statement_timeout to the check via SET LOCAL', async () => { - (db.$executeRaw as jest.Mock).mockReturnValue('SET_STATEMENT_TIMEOUT'); - (db.$queryRaw as jest.Mock).mockReturnValue('SELECT_1'); - (db.$transaction as jest.Mock).mockResolvedValue([undefined, [{ '?column?': 1 }]]); - const req = { method: 'GET' } as NextApiRequest; - const res = createMockRes(); - - await handler(req, res); - - const executeRawCall = (db.$executeRaw as jest.Mock).mock.calls[0] as - | [TemplateStringsArray, Prisma.Sql] - | undefined; - if (!executeRawCall) { - throw new Error('Expected db.$executeRaw to have been called'); - } - const [setTimeoutStrings, rawTimeoutValue] = executeRawCall; - expect(setTimeoutStrings.join('')).toContain('SET LOCAL statement_timeout = '); - expect(rawTimeoutValue.strings.join('')).toBe('3000'); - - const queryRawCall = (db.$queryRaw as jest.Mock).mock.calls[0] as - | [TemplateStringsArray] - | undefined; - if (!queryRawCall) { - throw new Error('Expected db.$queryRaw to have been called'); - } - const [selectStrings] = queryRawCall; - expect(selectStrings.join('')).toBe('SELECT 1'); - - // Order matters: the timeout must be set before SELECT 1 runs in the same transaction. - expect((db.$transaction as jest.Mock).mock.calls[0]?.[0]).toEqual([ - 'SET_STATEMENT_TIMEOUT', - 'SELECT_1', - ]); - }); - }); - - describe('database failures', () => { - it('responds with 503 when the database is unreachable', async () => { - (db.$transaction as jest.Mock).mockRejectedValue(new Error('connection refused')); - const req = { method: 'GET' } as NextApiRequest; - const res = createMockRes(); - - await handler(req, res); - - expect(res.status).toHaveBeenCalledWith(503); - expect(res.json).toHaveBeenCalledWith({ - status: 'error', - message: 'Database not reachable', - }); - }); - - it('responds with 503 when the database check times out', async () => { - (db.$transaction as jest.Mock).mockRejectedValue( - new Error('canceling statement due to statement timeout'), - ); - const req = { method: 'GET' } as NextApiRequest; - const res = createMockRes(); - - await handler(req, res); - - expect(res.status).toHaveBeenCalledWith(503); - }); - }); - - describe('method validation', () => { - it('rejects non-GET methods', async () => { - const req = { method: 'POST' } as NextApiRequest; - const res = createMockRes(); - - await handler(req, res); - - expect(res.status).toHaveBeenCalledWith(405); - expect((res.setHeader as jest.Mock).mock.calls).toContainEqual(['Allow', 'GET']); - expect((db.$executeRaw as jest.Mock).mock.calls).toHaveLength(0); - expect((db.$queryRaw as jest.Mock).mock.calls).toHaveLength(0); - expect((db.$transaction as jest.Mock).mock.calls).toHaveLength(0); - }); - }); -}); diff --git a/src/tests/simplify.test.ts b/src/tests/simplify.test.ts deleted file mode 100644 index 551f5d756..000000000 --- a/src/tests/simplify.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { type BalanceView as GroupBalance } from '~/server/db/types'; -import { addHours } from 'date-fns'; -import rawRealGraph from './mock/example_group_balances.json'; -import { simplifyDebts } from '~/lib/simplify'; - -const realGraph = rawRealGraph.map(({ amount, updatedAt, ...rest }) => ({ - ...rest, - amount: BigInt(amount), - updatedAt: new Date(updatedAt), - createdAt: new Date(updatedAt), -})); - -interface MinimalEdge { - userOne: number; - userTwo: number; - amount: bigint; -} - -const sortByIds = (a: GroupBalance, b: GroupBalance) => { - if (a.userId === b.userId) { - return a.friendId - b.friendId; - } - return a.userId - b.userId; -}; - -let dateCounter = 0; - -const edgeToGroupBalance = (edge: MinimalEdge): [GroupBalance, GroupBalance] => { - const createdAt = addHours(new Date(), dateCounter++); - const base = { - groupId: 0, - currency: 'USD', - updatedAt: createdAt, - createdAt, - }; - return [ - { - userId: edge.userOne, - friendId: edge.userTwo, - amount: edge.amount, - ...base, - }, - { - userId: edge.userTwo, - friendId: edge.userOne, - amount: 0n === edge.amount ? 0n : -edge.amount, - ...base, - }, - ]; -}; - -const padWithZeroBalances: (balances: GroupBalance[], userCount: number) => GroupBalance[] = ( - balances, - userCount, -) => { - const result = [...balances]; - for (let userId = 0; userId < userCount; userId++) { - for (let friendId = userId + 1; friendId < userCount; friendId++) { - const found = balances.find( - (balance) => balance.userId === userId && balance.friendId === friendId, - ); - - if (!found) { - result.push(...edgeToGroupBalance({ userOne: userId, userTwo: friendId, amount: 0n })); - } - } - } - return result; -}; - -const getFullBalanceGraph = (edges: MinimalEdge[], userCount: number): GroupBalance[] => { - const arr = padWithZeroBalances(edges.flatMap(edgeToGroupBalance), userCount); - arr.sort(sortByIds); - return arr; -}; - -// Taken from https://www.geeksforgeeks.org/minimize-cash-flow-among-given-set-friends-borrowed-money/ -const smallGraph: GroupBalance[] = getFullBalanceGraph( - [ - { userOne: 0, userTwo: 1, amount: 1000n }, - { userOne: 1, userTwo: 2, amount: 5000n }, - { userOne: 2, userTwo: 0, amount: -2000n }, - ], - 3, -); - -const smallGraphResult: GroupBalance[] = getFullBalanceGraph( - [ - { userOne: 1, userTwo: 2, amount: 4000n }, - { userOne: 2, userTwo: 0, amount: -3000n }, - ], - 3, -).map((resultBalance, idx) => ({ - ...resultBalance, - updatedAt: smallGraph[idx]!.updatedAt, - createdAt: smallGraph[idx]!.createdAt, -})); - -// Taken from https://medium.com/@mithunmk93/algorithm-behind-splitwises-debt-simplification-feature-8ac485e97688 -const largeGraph: GroupBalance[] = getFullBalanceGraph( - [ - { userOne: 1, userTwo: 2, amount: 4000n }, - { userOne: 1, userTwo: 5, amount: -1000n }, - { userOne: 1, userTwo: 6, amount: -3000n }, - - { userOne: 2, userTwo: 3, amount: 2000n }, - { userOne: 2, userTwo: 5, amount: -3000n }, - - { userOne: 3, userTwo: 4, amount: 5000n }, - { userOne: 3, userTwo: 5, amount: -1000n }, - { userOne: 3, userTwo: 6, amount: -1000n }, - - { userOne: 4, userTwo: 5, amount: -1000n }, - ], - 7, -); - -const denseGraph: GroupBalance[] = getFullBalanceGraph( - [ - { userOne: 0, userTwo: 1, amount: 895795n }, - { userOne: 0, userTwo: 2, amount: 328043n }, - { userOne: 0, userTwo: 3, amount: -174624n }, - { userOne: 0, userTwo: 4, amount: 5035n }, - { userOne: 0, userTwo: 5, amount: -81416n }, - - { userOne: 1, userTwo: 2, amount: -324560n }, - { userOne: 1, userTwo: 3, amount: -11502n }, - { userOne: 1, userTwo: 4, amount: 126115n }, - { userOne: 1, userTwo: 5, amount: 83333n }, - - { userOne: 2, userTwo: 3, amount: -519167n }, - { userOne: 2, userTwo: 4, amount: -400050n }, - { userOne: 2, userTwo: 5, amount: -23334n }, - - { userOne: 3, userTwo: 4, amount: 157284n }, - { userOne: 3, userTwo: 5, amount: 15833n }, - - { userOne: 4, userTwo: 5, amount: 12916n }, - ], - 6, -); - -describe('simplifyDebts', () => { - it('simplifies small graph', () => { - const result = simplifyDebts(smallGraph); - result.sort(sortByIds); - expect(result).toEqual(smallGraphResult); - }); - - it.each([ - { graph: smallGraph, expected: 2 }, - { graph: largeGraph, expected: 3 }, - { graph: denseGraph, expected: 5 }, - ])('gets the optimal operation count', ({ graph, expected }) => { - expect(simplifyDebts(graph).filter((balance) => 0 < balance.amount).length).toBe(expected); - }); - - it.each([ - ['small', smallGraph], - ['large', largeGraph], - ['dense', denseGraph], - ['real', realGraph], - ])('preserves metadata for %s graph', (name, graph) => { - const result = simplifyDebts(graph); - for (const balance of result) { - expect(balance.groupId).toBeDefined(); - expect(balance.currency).toBeDefined(); - expect(balance.userId).toBeDefined(); - expect(balance.friendId).toBeDefined(); - expect(balance.updatedAt).toBeDefined(); - } - }); - - it.each([ - ['small', smallGraph], - ['large', largeGraph], - ['dense', denseGraph], - ['real', realGraph], - ])('preserves the total balance per user for %s graph', (name, graph) => { - const startingBalances = graph.reduce( - (acc, balance) => { - acc[balance.userId] = (acc[balance.userId] ?? 0n) + balance.amount; - return acc; - }, - {} as Record, - ); - - const userBalances = simplifyDebts(graph).reduce( - (acc, balance) => { - acc[balance.userId] = (acc[balance.userId] ?? 0n) + balance.amount; - return acc; - }, - {} as Record, - ); - - expect(startingBalances).toEqual(userBalances); - }); - - it.each([ - ['small', smallGraph], - ['large', largeGraph], - ['dense', denseGraph], - ['real', realGraph], - ])('produces no self-loops for %s graph', (name, graph) => { - const result = simplifyDebts(graph); - for (const balance of result) { - expect(balance.userId).not.toBe(balance.friendId); - } - }); -}); diff --git a/test/setup.integration.ts b/test/setup.integration.ts new file mode 100644 index 000000000..4f24de72a --- /dev/null +++ b/test/setup.integration.ts @@ -0,0 +1,17 @@ +import { applyD1Migrations } from 'cloudflare:test'; +import { env } from 'cloudflare:workers'; +import { beforeAll } from 'vitest'; + +// BigInt values appear in tRPC results and balance rows. +// @ts-expect-error extending the BigInt prototype for tests only +BigInt.prototype.toJSON = function toJSON() { + return this.toString(); +}; + +beforeAll(async () => { + const { DB, TEST_MIGRATIONS } = env as unknown as { + DB: D1Database; + TEST_MIGRATIONS: Parameters[1]; + }; + await applyD1Migrations(DB, TEST_MIGRATIONS); +}); diff --git a/test/wrangler.test.jsonc b/test/wrangler.test.jsonc new file mode 100644 index 000000000..4cc58e1a3 --- /dev/null +++ b/test/wrangler.test.jsonc @@ -0,0 +1,20 @@ +{ + // Used only by the Vitest integration project (@cloudflare/vitest-pool-workers). + "name": "splitpro-test", + "compatibility_date": "2026-10-01", + "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"], + "d1_databases": [ + { + "binding": "DB", + "database_name": "splitpro-test", + "database_id": "00000000-0000-0000-0000-000000000000", + "migrations_dir": "../migrations", + }, + ], + "r2_buckets": [{ "binding": "UPLOADS", "bucket_name": "splitpro-test-uploads" }], + "vars": { + "SKIP_ENV_VALIDATION": "1", + "NEXTAUTH_URL": "http://localhost:8787", + "NEXTAUTH_SECRET": "test-secret-test-secret-test-secret-123456", + }, +} diff --git a/tests/e2e/app.spec.ts b/tests/e2e/app.spec.ts new file mode 100644 index 000000000..acb73de7b --- /dev/null +++ b/tests/e2e/app.spec.ts @@ -0,0 +1,220 @@ +import { expect, login, test, trpc } from './fixtures'; +import { USERS } from './users.mjs'; + +/** Superjson input with BigInt fields: `bigints` lists the dotted paths that hold bigints. */ +const sj = (json: unknown, bigints: string[] = []) => ({ + json, + meta: bigints.length + ? { values: Object.fromEntries(bigints.map((p) => [p, ['bigint']])) } + : undefined, +}); + +const expenseInput = (opts: { + paidBy: number; + other: number; + amount: number; + groupId?: number | null; + name?: string; + splitType?: string; + cronExpression?: string; +}) => + sj( + { + paidBy: opts.paidBy, + name: opts.name ?? 'E2E dinner', + category: 'food', + amount: String(opts.amount), + groupId: opts.groupId ?? null, + splitType: opts.splitType ?? 'EQUAL', + currency: 'EUR', + participants: [ + { userId: opts.paidBy, amount: String(opts.amount / 2) }, + { userId: opts.other, amount: String(-opts.amount / 2) }, + ], + cronExpression: opts.cronExpression, + }, + ['amount', 'participants.0.amount', 'participants.1.amount'], + ); + +// oxlint-disable-next-line typescript/no-explicit-any +const data = (result: { body: unknown }): any => + // oxlint-disable-next-line typescript/no-explicit-any + (result.body as { result: { data: { json: any } } }[])[0].result.data.json; + +// Tests share one database but never share rows: each one works on its own group or expense, +// So they can run in parallel. + +test.describe('auth', () => { + test('redirects visitors to sign in, which offers Google only', async ({ page }) => { + await page.goto('/balances'); + await expect(page).toHaveURL(/\/auth\/signin/); + await expect(page.getByText('Continue with Google')).toBeVisible(); + }); + + test('a signed-in user sees the app instead of the sign-in page', async ({ + page, + context, + baseURL, + }) => { + await login(context, 'alice', baseURL!); + await page.goto('/balances'); + await expect(page).toHaveURL(/\/balances/); + await expect(page.getByText('Continue with Google')).toHaveCount(0); + }); + + test('tRPC rejects anonymous calls and serves signed-in ones', async ({ context, baseURL }) => { + expect((await trpc(context, baseURL!, 'query', 'user.me')).status).toBe(401); + await login(context, 'alice', baseURL!); + const me = await trpc(context, baseURL!, 'query', 'user.me'); + expect(me.status).toBe(200); + expect(data(me)).toMatchObject({ id: USERS.alice.id, email: USERS.alice.email }); + }); +}); + +test.describe('groups, expenses and balances', () => { + test('creates a group and an expense, shows both in the UI, and settles up', async ({ + page, + context, + baseURL, + }) => { + await login(context, 'alice', baseURL!); + const groupName = `Trip ${Date.now()}`; + + const created = await trpc( + context, + baseURL!, + 'mutation', + 'group.create', + sj({ name: groupName }), + ); + expect(created.status).toBe(200); + const group = data(created); + await trpc( + context, + baseURL!, + 'mutation', + 'group.addMembers', + sj({ groupId: group.id, userIds: [USERS.bob.id] }), + ); + + // BigInt + Date round trip through the real tRPC endpoint. + const add = await trpc( + context, + baseURL!, + 'mutation', + 'expense.addOrEditExpense', + expenseInput({ + paidBy: USERS.alice.id, + other: USERS.bob.id, + amount: 1000, + groupId: group.id, + }), + ); + expect(add.status).toBe(200); + + await page.goto(`/groups/${group.id}`); + await expect(page.getByText(groupName).first()).toBeVisible(); + await expect(page.getByText('E2E dinner').first()).toBeVisible(); + + const groupBalances = async (): Promise< + { userId: number; friendId: number; amount: string }[] + > => + data( + await trpc(context, baseURL!, 'query', 'group.getGroupDetails', sj({ groupId: group.id })), + ).groupBalances; + const before = (await groupBalances()).find( + (b) => b.userId === USERS.alice.id && b.friendId === USERS.bob.id, + ); + expect(before?.amount).toBe('500'); + + await page.goto('/balances'); + await expect(page.getByText(USERS.bob.name).first()).toBeVisible(); + + // Bob settles up; the group balance goes back to zero. + await context.clearCookies(); + await login(context, 'bob', baseURL!); + const settle = await trpc( + context, + baseURL!, + 'mutation', + 'expense.addOrEditExpense', + expenseInput({ + paidBy: USERS.bob.id, + other: USERS.alice.id, + amount: 1000, + groupId: group.id, + splitType: 'SETTLEMENT', + name: 'Settle up', + }), + ); + expect(settle.status).toBe(200); + expect((await groupBalances()).every((b) => '0' === b.amount)).toBe(true); + }); + + test('a stranger cannot read or delete someone else’s expense', async ({ context, baseURL }) => { + await login(context, 'alice', baseURL!); + const add = await trpc( + context, + baseURL!, + 'mutation', + 'expense.addOrEditExpense', + expenseInput({ paidBy: USERS.alice.id, other: USERS.bob.id, amount: 400, name: 'Private' }), + ); + const expenseId = data(add)[0].id as string; + + await context.clearCookies(); + await login(context, 'mallory', baseURL!); + const read = await trpc( + context, + baseURL!, + 'query', + 'expense.getExpenseDetails', + sj({ expenseId }), + ); + expect(read.status).toBe(404); + const del = await trpc( + context, + baseURL!, + 'mutation', + 'expense.deleteExpense', + sj({ expenseId }), + ); + expect(del.status).toBeGreaterThanOrEqual(400); + + await context.clearCookies(); + await login(context, 'alice', baseURL!); + const still = await trpc( + context, + baseURL!, + 'query', + 'expense.getExpenseDetails', + sj({ expenseId }), + ); + expect(still.status).toBe(200); + expect(data(still).deletedAt).toBeNull(); + }); + + test('creates a recurring expense and lists it (cron parsing works in the production bundle)', async ({ + context, + baseURL, + }) => { + await login(context, 'alice', baseURL!); + const name = `Monthly rent ${Date.now()}`; + const add = await trpc( + context, + baseURL!, + 'mutation', + 'expense.addOrEditExpense', + expenseInput({ + paidBy: USERS.alice.id, + other: USERS.bob.id, + amount: 800, + name, + cronExpression: '0 9 1 * *', + }), + ); + expect(add.status).toBe(200); + const recurring = data(await trpc(context, baseURL!, 'query', 'expense.getRecurringExpenses')); + expect(JSON.stringify(recurring)).toContain(name); + }); +}); diff --git a/tests/e2e/fixtures.ts b/tests/e2e/fixtures.ts new file mode 100644 index 000000000..202dd3c4f --- /dev/null +++ b/tests/e2e/fixtures.ts @@ -0,0 +1,40 @@ +import { type BrowserContext, test as base, expect } from '@playwright/test'; + +import { USERS, sessionCookie } from './users.mjs'; + +type Who = keyof typeof USERS; + +export const login = async (context: BrowserContext, who: Who, baseURL: string) => { + await context.addCookies([ + { + name: 'better-auth.session_token', + value: sessionCookie(USERS[who].token), + url: baseURL, + }, + ]); +}; + +/** Calls a tRPC procedure over HTTP the way the browser does (superjson, single call). */ +export const trpc = async ( + context: BrowserContext, + baseURL: string, + kind: 'query' | 'mutation', + path: string, + input?: unknown, +) => { + const body = input === undefined ? { json: null, meta: { values: ['undefined'], v: 1 } } : input; + const response = + kind === 'query' + ? await context.request.get( + `${baseURL}/api/trpc/${path}?batch=1&input=${encodeURIComponent(JSON.stringify({ 0: body }))}`, + ) + : await context.request.post(`${baseURL}/api/trpc/${path}?batch=1`, { data: { 0: body } }); + const parsed = (await response.json()) as unknown; + if (response.status() >= 400) { + console.log(`tRPC ${path} -> ${response.status()}`, JSON.stringify(parsed).slice(0, 600)); + } + return { status: response.status(), body: parsed }; +}; + +export { expect }; +export const test = base; diff --git a/tests/e2e/prepare.mjs b/tests/e2e/prepare.mjs new file mode 100644 index 000000000..6892dd244 --- /dev/null +++ b/tests/e2e/prepare.mjs @@ -0,0 +1,36 @@ +// Creates a throwaway local D1 under .e2e-state (migrations + three users with live sessions). +// Runs as part of the Playwright webServer command, before the Worker starts. +import { execFileSync } from 'node:child_process'; +import { rmSync, writeFileSync } from 'node:fs'; + +import { USERS } from './users.mjs'; + +const wrangler = (...args) => + execFileSync( + 'npx', + [ + 'wrangler', + ...args, + '-c', + 'dist/splitpro/wrangler.json', + '--local', + '--persist-to', + '.e2e-state', + ], + { stdio: 'pipe', env: { ...process.env, CI: '1' } }, + ); + +rmSync('.e2e-state', { recursive: true, force: true }); +wrangler('d1', 'migrations', 'apply', 'splitpro'); + +const now = Date.now(); +const expires = now + 7 * 86_400_000; +const sql = Object.values(USERS) + .flatMap((u) => [ + `INSERT INTO "User" (id, name, email, emailVerified, currency, createdAt, updatedAt) VALUES (${u.id}, '${u.name}', '${u.email}', 1, 'EUR', ${now}, ${now});`, + `INSERT INTO "Session" (token, userId, expiresAt, createdAt, updatedAt) VALUES ('${u.token}', ${u.id}, ${expires}, ${now}, ${now});`, + ]) + .join('\n'); +writeFileSync('.e2e-seed.sql', sql); +wrangler('d1', 'execute', 'splitpro', '--file', '.e2e-seed.sql'); +rmSync('.e2e-seed.sql'); diff --git a/tests/e2e/users.mjs b/tests/e2e/users.mjs new file mode 100644 index 000000000..f277c2fdc --- /dev/null +++ b/tests/e2e/users.mjs @@ -0,0 +1,12 @@ +import { createHmac } from 'node:crypto'; + +export const SECRET = 'e2e-secret-e2e-secret-e2e-secret-123456'; +export const USERS = { + alice: { id: 1, name: 'Alice E2E', email: 'alice@e2e.test', token: 'e2e-token-alice' }, + bob: { id: 2, name: 'Bob E2E', email: 'bob@e2e.test', token: 'e2e-token-bob' }, + mallory: { id: 3, name: 'Mallory E2E', email: 'mallory@e2e.test', token: 'e2e-token-mallory' }, +}; + +/** better-auth cookie value: `.`, URL-encoded. */ +export const sessionCookie = (token) => + encodeURIComponent(`${token}.${createHmac('sha256', SECRET).update(token).digest('base64')}`); diff --git a/tsconfig.json b/tsconfig.json index 8b23757d8..1930d0536 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -46,7 +46,10 @@ "src/tests", "src/**/*.test.ts", "src/dummies", - "jest.config.ts", - "jest.setup.ts" + "test", + "tests", + "playwright.config.ts", + "vitest.config.ts", + ".e2e-state" ] } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 000000000..7571f652e --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,34 @@ +import { resolve } from 'node:path'; + +import { cloudflareTest, readD1Migrations } from '@cloudflare/vitest-plugin'; +import { defineConfig } from 'vitest/config'; + +const alias = { '~': resolve(import.meta.dirname, 'src') }; + +export default defineConfig({ + test: { + projects: [ + { + // Real D1 (local, via workerd) with the production migrations applied. + resolve: { alias }, + plugins: [ + cloudflareTest(async () => ({ + wrangler: { configPath: './test/wrangler.test.jsonc' }, + miniflare: { + bindings: { + TEST_MIGRATIONS: await readD1Migrations(resolve(import.meta.dirname, 'migrations')), + }, + }, + })), + ], + test: { + name: 'integration', + include: ['src/tests/integration/**/*.integration.test.ts'], + setupFiles: ['test/setup.integration.ts'], + // CommonJS-only dependencies must be pre-bundled to run inside workerd. + deps: { optimizer: { ssr: { enabled: true, include: ['cron-parser'] } } }, + }, + }, + ], + }, +});