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