diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 5cc444a..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "permissions": { - "allow": [ - "mcp__context7__query-docs", - "Skill(pr-review-toolkit:review-pr)", - "Bash(git ls-tree:*)", - "Skill(commit)", - "Skill(feature-dev:feature-dev)", - "Bash(bunx:*)", - "Bash(npx @tailwindcss/upgrade --force)", - "Bash(xargs:*)", - "WebFetch(domain:avvvatars.com)", - "Bash(bun add:*)" - ] - } -} diff --git a/.eslintrc.cjs b/.eslintrc.cjs index fcd5bed..7e59b46 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,6 +1,6 @@ /** @type {import('eslint').Linter.Config} */ const config = { - ignorePatterns: ['apps/**', 'packages/**'], + ignorePatterns: ['apps/**', 'packages/**', 'tests/playwright-report/**', 'tests/test-results/**', '**/.eslintrc.cjs'], extends: ['formbase/base'], }; diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..25a2c9c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,144 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + unit-integration: + name: Unit & Integration Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run tests + run: bun run test + working-directory: tests + + - name: Run tests with coverage + run: bun run test:coverage + working-directory: tests + + - name: Upload coverage reports + uses: codecov/codecov-action@v4 + with: + directory: tests/coverage + fail_ci_if_error: false + verbose: true + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + e2e: + name: E2E Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Install Playwright browsers + run: bunx playwright install --with-deps chromium + working-directory: tests + + - name: Build application + run: bun run build + working-directory: apps/web + env: + DATABASE_URL: file:./test.db + DATABASE_AUTH_TOKEN: '' + BETTER_AUTH_SECRET: test-secret-for-ci-at-least-32-chars + RESEND_API_KEY: '' + ALLOW_SIGNIN_SIGNUP: 'true' + NEXT_PUBLIC_APP_URL: http://localhost:3000 + + - name: Setup test database + run: bunx drizzle-kit push + working-directory: packages/db + env: + DATABASE_URL: file:../../apps/web/test.db + DATABASE_AUTH_TOKEN: '' + SKIP_ENV_VALIDATION: 'true' + + - name: Seed E2E test data + run: bun run e2e/seed.ts + working-directory: tests + env: + DATABASE_URL: file:../apps/web/test.db + DATABASE_AUTH_TOKEN: '' + BETTER_AUTH_SECRET: test-secret-for-ci-at-least-32-chars + ALLOW_SIGNIN_SIGNUP: 'true' + NEXT_PUBLIC_APP_URL: http://localhost:3000 + + - name: Start application + run: | + bun run start & + sleep 10 + working-directory: apps/web + env: + DATABASE_URL: file:./test.db + DATABASE_AUTH_TOKEN: '' + BETTER_AUTH_SECRET: test-secret-for-ci-at-least-32-chars + RESEND_API_KEY: '' + ALLOW_SIGNIN_SIGNUP: 'true' + NEXT_PUBLIC_APP_URL: http://localhost:3000 + PORT: 3000 + + - name: Run E2E tests + run: bun run test:e2e + working-directory: tests + env: + BASE_URL: http://localhost:3000 + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-report + path: tests/playwright-report/ + retention-days: 7 + + lint: + name: Lint & Type Check + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run lint + run: bun run lint + + - name: Run type check + run: bun run typecheck diff --git a/.gitignore b/.gitignore index bd1df71..7f811ba 100644 --- a/.gitignore +++ b/.gitignore @@ -169,4 +169,5 @@ formbase-new bun.lockb *.db -.vscode \ No newline at end of file +.claude/ +.vscode/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 5adaac2..26224aa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "projectColors.mainColor": "#1a6bb7", - "window.title": "shadcn", + "window.title": "tests", "workbench.colorCustomizations": { "statusBarItem.warningBackground": "#1a6bb7", "statusBarItem.warningForeground": "#ffffff", diff --git a/bun.lock b/bun.lock index 8938a7c..044d92e 100644 --- a/bun.lock +++ b/bun.lock @@ -8,8 +8,11 @@ "@formbase/tsconfig": "workspace:*", "@ianvs/prettier-plugin-sort-imports": "^4.2.1", "@types/eslint": "^8.56.10", + "@typescript-eslint/eslint-plugin": "^7.10.0", + "@typescript-eslint/parser": "^7.10.0", "eslint": "^8.57.0", "eslint-config-formbase": "workspace:*", + "eslint-plugin-eslint-comments": "^3.2.0", "prettier": "^3.2.5", "prettier-plugin-astro": "^0.13.0", "prettier-plugin-curly": "^0.2.1", @@ -138,10 +141,12 @@ "packages/config/eslint": { "name": "eslint-config-formbase", "version": "0.1.0", - "devDependencies": { - "@formbase/tsconfig": "workspace:*", + "dependencies": { "@typescript-eslint/eslint-plugin": "^7.10.0", "@typescript-eslint/parser": "^7.10.0", + }, + "devDependencies": { + "@formbase/tsconfig": "workspace:*", "astro-eslint-parser": "^1.0.2", "eslint-config-next": "^16.1.1", "eslint-config-prettier": "^9.1.0", @@ -289,10 +294,28 @@ "eslint-config-formbase": "workspace:*", }, }, + "tests": { + "name": "@formbase/tests", + "version": "0.1.0", + "devDependencies": { + "@formbase/api": "workspace:*", + "@formbase/db": "workspace:*", + "@formbase/utils": "workspace:*", + "@libsql/client": "^0.14.0", + "@playwright/test": "^1.49.0", + "@types/node": "^20.17.0", + "@vitest/coverage-v8": "^2.1.8", + "drizzle-orm": "^0.30.10", + "typescript": "5.8.2", + "vitest": "^2.1.8", + }, + }, }, "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + "@astrojs/check": ["@astrojs/check@0.9.6", "", { "dependencies": { "@astrojs/language-server": "^2.16.1", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "bin/astro-check.js" } }, "sha512-jlaEu5SxvSgmfGIFfNgcn5/f+29H61NJzEMfAZ82Xopr4XBchXB1GVlcJsE+elUlsYSbXlptZLX+JMG3b/wZEA=="], "@astrojs/compiler": ["@astrojs/compiler@1.8.2", "", {}, "sha512-o/ObKgtMzl8SlpIdzaxFnt7SATKPxu4oIP/1NL+HDJRzxfJcAkOTAb/ZKMRyULbz4q+1t2/DAebs2Z1QairkZw=="], @@ -323,53 +346,53 @@ "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - "@aws-sdk/client-ses": ["@aws-sdk/client-ses@3.964.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.964.0", "@aws-sdk/credential-provider-node": "3.964.0", "@aws-sdk/middleware-host-header": "3.957.0", "@aws-sdk/middleware-logger": "3.957.0", "@aws-sdk/middleware-recursion-detection": "3.957.0", "@aws-sdk/middleware-user-agent": "3.964.0", "@aws-sdk/region-config-resolver": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@aws-sdk/util-user-agent-browser": "3.957.0", "@aws-sdk/util-user-agent-node": "3.964.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-gAbDlXvNP5Sb2tS4tJYmOS6/frmba2tryJ4MzJVeR1ad8sSa94GQx7XbR7HyCi5VtJpOSk7Uibp3aKzK3+sWsg=="], + "@aws-sdk/client-ses": ["@aws-sdk/client-ses@3.965.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.965.0", "@aws-sdk/credential-provider-node": "3.965.0", "@aws-sdk/middleware-host-header": "3.965.0", "@aws-sdk/middleware-logger": "3.965.0", "@aws-sdk/middleware-recursion-detection": "3.965.0", "@aws-sdk/middleware-user-agent": "3.965.0", "@aws-sdk/region-config-resolver": "3.965.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-endpoints": "3.965.0", "@aws-sdk/util-user-agent-browser": "3.965.0", "@aws-sdk/util-user-agent-node": "3.965.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-RZXJBoHA3I6Ts1/bjOLDceT0hbK00lVkXAXFpcz5At+p6Yu52jVmdAdKDmLuf1IgCDw7s2IsrR4Us2Od1AabCg=="], - "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.964.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.964.0", "@aws-sdk/middleware-host-header": "3.957.0", "@aws-sdk/middleware-logger": "3.957.0", "@aws-sdk/middleware-recursion-detection": "3.957.0", "@aws-sdk/middleware-user-agent": "3.964.0", "@aws-sdk/region-config-resolver": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@aws-sdk/util-user-agent-browser": "3.957.0", "@aws-sdk/util-user-agent-node": "3.964.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-IenVyY8Io2CwBgmS22xk/H5LibmSbvLnPA9oFqLORO6Ji1Ks8z/ow+ud/ZurVjFekz3LD/uxVFX3ZKGo6N7Byw=="], + "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.965.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.965.0", "@aws-sdk/middleware-host-header": "3.965.0", "@aws-sdk/middleware-logger": "3.965.0", "@aws-sdk/middleware-recursion-detection": "3.965.0", "@aws-sdk/middleware-user-agent": "3.965.0", "@aws-sdk/region-config-resolver": "3.965.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-endpoints": "3.965.0", "@aws-sdk/util-user-agent-browser": "3.965.0", "@aws-sdk/util-user-agent-node": "3.965.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-iv2tr+n4aZ+nPUFFvG00hISPuEd4DU+1/Q8rPAYKXsM+vEPJ2nAnP5duUOa2fbOLIUCRxX3dcQaQaghVHDHzQw=="], - "@aws-sdk/core": ["@aws-sdk/core@3.964.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@aws-sdk/xml-builder": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-1gIfbt0KRxI8am1UYFcIxQ5QKb22JyN3k52sxyrKXJYC8Knn/rTUAZbYti45CfETe5PLadInGvWqClwGRlZKNg=="], + "@aws-sdk/core": ["@aws-sdk/core@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@aws-sdk/xml-builder": "3.965.0", "@smithy/core": "^3.20.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aq9BhQxdHit8UUJ9C0im9TtuKeK0pT6NXmNJxMTCFeStI7GG7ImIsSislg3BZTIifVg1P6VLdzMyz9de85iutQ=="], - "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.964.0", "", { "dependencies": { "@aws-sdk/core": "3.964.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-jWNSXOOBMYuxzI2rXi8x91YL07dhomyGzzh0CdaLej0LRmknmDrZcZNkVpa7Fredy1PFcmOlokwCS5PmZMN8ZQ=="], + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-mdGnaIjMxTIjsb70dEj3VsWPWpoq1V5MWzBSfJq2H8zgMBXjn6d5/qHP8HMf53l9PrsgqzMpXGv3Av549A2x1g=="], - "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.964.0", "", { "dependencies": { "@aws-sdk/core": "3.964.0", "@aws-sdk/types": "3.957.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/node-http-handler": "^4.4.7", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-up7dl6vcaoXuYSwGXDvx8RnF8Lwj3jGChhyUR7krZOXLarIfUUN3ILOZnVNK5s/HnVNkEILlkdPvjhr9LVC1/Q=="], + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/node-http-handler": "^4.4.7", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-YuGQel9EgA/z25oeLM+GYYQS750+8AESvr7ZEmVnRPL0sg+K3DmGqdv+9gFjFd0UkLjTlC/jtbP2cuY6UcPiHQ=="], - "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.964.0", "", { "dependencies": { "@aws-sdk/core": "3.964.0", "@aws-sdk/credential-provider-env": "3.964.0", "@aws-sdk/credential-provider-http": "3.964.0", "@aws-sdk/credential-provider-login": "3.964.0", "@aws-sdk/credential-provider-process": "3.964.0", "@aws-sdk/credential-provider-sso": "3.964.0", "@aws-sdk/credential-provider-web-identity": "3.964.0", "@aws-sdk/nested-clients": "3.964.0", "@aws-sdk/types": "3.957.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-t4FN9qTWU4nXDU6EQ6jopvyhXw0dbQ3n+3g6x5hmc1ECFAqA+xmFd1i5LljdZCi79cUXHduQWwvW8RJHMf0qJw=="], + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/credential-provider-env": "3.965.0", "@aws-sdk/credential-provider-http": "3.965.0", "@aws-sdk/credential-provider-login": "3.965.0", "@aws-sdk/credential-provider-process": "3.965.0", "@aws-sdk/credential-provider-sso": "3.965.0", "@aws-sdk/credential-provider-web-identity": "3.965.0", "@aws-sdk/nested-clients": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-xRo72Prer5s0xYVSCxCymVIRSqrVlevK5cmU0GWq9yJtaBNpnx02jwdJg80t/Ni7pgbkQyFWRMcq38c1tc6M/w=="], - "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.964.0", "", { "dependencies": { "@aws-sdk/core": "3.964.0", "@aws-sdk/nested-clients": "3.964.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-c64dmTizMkJXDRzN3NYPTmUpKxegr5lmLOYPeQ60Zcbft6HFwPme8Gwy8pNxO4gG1fw6Ja2Vu6fZuSTn8aDFOQ=="], + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/nested-clients": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-43/H8Qku8LHyugbhLo8kjD+eauhybCeVkmrnvWl8bXNHJP7xi1jCdtBQJKKJqiIHZws4MOEwkji8kFdAVRCe6g=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.964.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.964.0", "@aws-sdk/credential-provider-http": "3.964.0", "@aws-sdk/credential-provider-ini": "3.964.0", "@aws-sdk/credential-provider-process": "3.964.0", "@aws-sdk/credential-provider-sso": "3.964.0", "@aws-sdk/credential-provider-web-identity": "3.964.0", "@aws-sdk/types": "3.957.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-FHxDXPOj888/qc/X8s0x4aUBdp4Y3k9VePRehUJBWRhhTsAyuIJis5V0iQeY1qvtqHXYa2qd1EZHGJ3bTjHxSw=="], + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.965.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.965.0", "@aws-sdk/credential-provider-http": "3.965.0", "@aws-sdk/credential-provider-ini": "3.965.0", "@aws-sdk/credential-provider-process": "3.965.0", "@aws-sdk/credential-provider-sso": "3.965.0", "@aws-sdk/credential-provider-web-identity": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-cRxmMHF+Zh2lkkkEVduKl+8OQdtg/DhYA69+/7SPSQURlgyjFQGlRQ58B7q8abuNlrGT3sV+UzeOylZpJbV61Q=="], - "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.964.0", "", { "dependencies": { "@aws-sdk/core": "3.964.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-HaTLKqj3jeZY88E/iBjsNJsXgmRTTT7TghqeRiF8FKb/7UY1xEvasBO0c1xqfOye8dsyt35nTfTTyIsd/CBfww=="], + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-gmkPmdiR0yxnTzLPDb7rwrDhGuCUjtgnj8qWP+m0gSz/W43rR4jRPVEf6DUX2iC+ImQhxo3NFhuB3V42Kzo3TQ=="], - "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.964.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.964.0", "@aws-sdk/core": "3.964.0", "@aws-sdk/token-providers": "3.964.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-oR78TjSpjVf1IpPWQnGHEGqlnQs+K4f5nCxLK2P6JDPprXay6oknsoSiU4x2urav6VCyMPMC9KTCGjBoFKUIxQ=="], + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.965.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.965.0", "@aws-sdk/core": "3.965.0", "@aws-sdk/token-providers": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-N01AYvtCqG3Wo/s/LvYt19ity18/FqggiXT+elAs3X9Om/Wfx+hw9G+i7jaDmy+/xewmv8AdQ2SK5Q30dXw/Fw=="], - "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.964.0", "", { "dependencies": { "@aws-sdk/core": "3.964.0", "@aws-sdk/nested-clients": "3.964.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-07JQDmbjZjOt3nL/j1wTcvQqjmPkynQYftUV/ooZ+qTbmJXFbCBdal1VCElyeiu0AgBq9dfhw0rBBcbND1ZMlA=="], + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/nested-clients": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-T4gMZ2JzXnfxe1oTD+EDGLSxFfk1+WkLZdiHXEMZp8bFI1swP/3YyDFXI+Ib9Uq1JhnAmrCXtOnkicKEhDkdhQ=="], - "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-BBgKawVyfQZglEkNTuBBdC3azlyqNXsvvN4jPkWAiNYcY0x1BasaJFl+7u/HisfULstryweJq/dAvIZIxzlZaA=="], + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-SfpSYqoPOAmdb3DBsnNsZ0vix+1VAtkUkzXM79JL3R5IfacpyKE2zytOgVAQx/FjhhlpSTwuXd+LRhUEVb3MaA=="], - "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-w1qfKrSKHf9b5a8O76yQ1t69u6NWuBjr5kBX+jRWFx/5mu6RLpqERXRpVJxfosbep7k3B+DSB5tZMZ82GKcJtQ=="], + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-gjUvJRZT1bUABKewnvkj51LAynFrfz2h5DYAg5/2F4Utx6UOGByTSr9Rq8JCLbURvvzAbCtcMkkIJRxw+8Zuzw=="], - "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-D2H/WoxhAZNYX+IjkKTdOhOkWQaK0jjJrDBj56hKjU5c9ltQiaX/1PqJ4dfjHntEshJfu0w+E6XJ+/6A6ILBBA=="], + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-6dvD+18Ni14KCRu+tfEoNxq1sIGVp9tvoZDZ7aMvpnA7mDXuRLrOjRQ/TAZqXwr9ENKVGyxcPl0cRK8jk1YWjA=="], - "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.964.0", "", { "dependencies": { "@aws-sdk/core": "3.964.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-/QyBl8WLNtqw3ucyAggumQXVCi8GRxaDGE1ElyYMmacfiwHl37S9y8JVW/QLL1lIEXGcsrhMUKV3pyFJFALA7w=="], + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-endpoints": "3.965.0", "@smithy/core": "^3.20.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-RBEYVGgu/WeAt+H/qLrGc+t8LqAUkbyvh3wBfTiuAD+uBcWsKnvnB1iSBX75FearC0fmoxzXRUc0PMxMdqpjJQ=="], - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.964.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.964.0", "@aws-sdk/middleware-host-header": "3.957.0", "@aws-sdk/middleware-logger": "3.957.0", "@aws-sdk/middleware-recursion-detection": "3.957.0", "@aws-sdk/middleware-user-agent": "3.964.0", "@aws-sdk/region-config-resolver": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@aws-sdk/util-user-agent-browser": "3.957.0", "@aws-sdk/util-user-agent-node": "3.964.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ql+ftRwjyZkZeG3qbrRJFVmNR0id83WEUqhFVjvrQMWspNApBhz0Ar4YVSn7Uv0QaKkaR7ALPtmdMzFr3/E4bQ=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.965.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.965.0", "@aws-sdk/middleware-host-header": "3.965.0", "@aws-sdk/middleware-logger": "3.965.0", "@aws-sdk/middleware-recursion-detection": "3.965.0", "@aws-sdk/middleware-user-agent": "3.965.0", "@aws-sdk/region-config-resolver": "3.965.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-endpoints": "3.965.0", "@aws-sdk/util-user-agent-browser": "3.965.0", "@aws-sdk/util-user-agent-node": "3.965.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-muNVUjUEU+/KLFrLzQ8PMXyw4+a/MP6t4GIvwLtyx/kH0rpSy5s0YmqacMXheuIe6F/5QT8uksXGNAQenitkGQ=="], - "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-V8iY3blh8l2iaOqXWW88HbkY5jDoWjH56jonprG/cpyqqCnprvpMUZWPWYJoI8rHRf2bqzZeql1slxG6EnKI7A=="], + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/config-resolver": "^4.4.5", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-RoMhu9ly2B0coxn8ctXosPP2WmDD0MkQlZGLjoYHQUOCBmty5qmCxOqBmBDa6wbWbB8xKtMQ/4VXloQOgzjHXg=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.964.0", "", { "dependencies": { "@aws-sdk/core": "3.964.0", "@aws-sdk/nested-clients": "3.964.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-UqouLQbYepZnMFJGB/DVpA5GhF9uT98vNWSMz9PVbhgEPUKa73FECRT6YFZvZOh8kA+0JiENrnmS6d93I70ykQ=="], + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/nested-clients": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-aR0qxg0b8flkXJVE+CM1gzo7uJ57md50z2eyCwofC0QIz5Y0P7/7vvb9/dmUQt6eT9XRN5iRcUqq2IVxVDvJOw=="], - "@aws-sdk/types": ["@aws-sdk/types@3.957.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg=="], + "@aws-sdk/types": ["@aws-sdk/types@3.965.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ=="], - "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-endpoints": "^3.2.7", "tslib": "^2.6.2" } }, "sha512-xwF9K24mZSxcxKS3UKQFeX/dPYkEps9wF1b+MGON7EvnbcucrJGyQyK1v1xFPn1aqXkBTFi+SZaMRx5E5YCVFw=="], + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-endpoints": "^3.2.7", "tslib": "^2.6.2" } }, "sha512-WqSCB0XIsGUwZWvrYkuoofi2vzoVHqyeJ2kN+WyoOsxPLTiQSBIoqm/01R/qJvoxwK/gOOF7su9i84Vw2NQQpQ=="], - "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.957.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nhmgKHnNV9K+i9daumaIz8JTLsIIML9PE/HUks5liyrjUzenjW/aHoc7WJ9/Td/gPZtayxFnXQSJRb/fDlBuJw=="], + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9LJFand4bIoOjOF4x3wx0UZYiFZRo4oUauxQSiEX2dVg+5qeBOJSjp2SeWykIE6+6frCZ5wvWm2fGLK8D32aJw=="], - "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-exueuwxef0lUJRnGaVkNSC674eAiWU07ORhxBnevFFZEKisln+09Qrtw823iyv5I1N8T+wKfh95xvtWQrNKNQw=="], + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/types": "^4.11.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-Xiza/zMntQGpkd2dETQeAK8So1pg5+STTzpcdGWxj5q0jGO5ayjqT/q1Q7BrsX5KIr6PvRkl9/V7lLCv04wGjQ=="], - "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.964.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.964.0", "@aws-sdk/types": "3.957.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-jgob8Z/bZIh1dwEgLqE12q+aCf0ieLy7anT8bWpqMijMJqsnrPBToa7smSykfom9YHrdOgrQhXswMpE75dzLRw=="], + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.965.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-kokIHUfNT3/P55E4fUJJrFHuuA9BbjFKUIxoLrd3UaRfdafT0ScRfg2eaZie6arf60EuhlUIZH0yALxttMEjxQ=="], - "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.957.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-Ai5iiQqS8kJ5PjzMhWcLKN0G2yasAkvpnPlq2EnqlIMdB48HsizElt62qcktdxp4neRMyGkFq4NzgmDbXnhRiA=="], + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.965.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-Tcod25/BTupraQwtb+Q+GX8bmEZfxIFjjJ/AvkhUZsZlkPeVluzq1uu3Oeqf145DCdMjzLIN6vab5MrykbDP+g=="], "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.2", "", {}, "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg=="], @@ -411,6 +434,8 @@ "@base-ui/utils": ["@base-ui/utils@0.2.3", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-/CguQ2PDaOzeVOkllQR8nocJ0FFIDqsWIcURsVmm53QGo8NhFNpePjNlyPIB41luxfOqnG7PU0xicMEw3ls7XQ=="], + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="], + "@better-auth/core": ["@better-auth/core@1.4.10", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-AThrfb6CpG80wqkanfrbN2/fGOYzhGladHFf3JhaWt/3/Vtf4h084T6PJLrDE7M/vCCGYvDI1DkvP3P1OB2HAg=="], "@better-auth/telemetry": ["@better-auth/telemetry@1.4.10", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.10" } }, "sha512-Dq4XJX6EKsUu0h3jpRagX739p/VMOTcnJYWRrLtDYkqtZFg+sFiFsSWVcfapZoWpRSUGYX9iKwl6nDHn6Ju2oQ=="], @@ -533,6 +558,8 @@ "@formbase/tailwind": ["@formbase/tailwind@workspace:packages/config/tailwind"], + "@formbase/tests": ["@formbase/tests@workspace:tests"], + "@formbase/tsconfig": ["@formbase/tsconfig@workspace:packages/config/tsconfig"], "@formbase/ui": ["@formbase/ui@workspace:packages/ui"], @@ -609,6 +636,8 @@ "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + "@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -701,6 +730,8 @@ "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], + "@playwright/test": ["@playwright/test@1.57.0", "", { "dependencies": { "playwright": "1.57.0" }, "bin": { "playwright": "cli.js" } }, "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA=="], + "@radix-ui/colors": ["@radix-ui/colors@3.0.0", "", {}, "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], @@ -867,7 +898,7 @@ "@smithy/config-resolver": ["@smithy/config-resolver@4.4.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg=="], - "@smithy/core": ["@smithy/core@3.20.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.8", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ=="], + "@smithy/core": ["@smithy/core@3.20.1", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.8", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-wOboSEdQ85dbKAJ0zL+wQ6b0HTSBRhtGa0PYKysQXkRg+vK0tdCRRVruiFM2QMprkOQwSYOnwF4og96PAaEGag=="], "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.7", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA=="], @@ -881,9 +912,9 @@ "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.1", "", { "dependencies": { "@smithy/core": "^3.20.0", "@smithy/middleware-serde": "^4.2.8", "@smithy/node-config-provider": "^4.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.2", "", { "dependencies": { "@smithy/core": "^3.20.1", "@smithy/middleware-serde": "^4.2.8", "@smithy/node-config-provider": "^4.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-mqpAdux0BNmZu/SqkFhQEnod4fX23xxTvU2LUpmKp0JpSI+kPYCiHJMmzREr8yxbNxKL2/DU1UZm9i++ayU+2g=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.17", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/service-error-classification": "^4.2.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.18", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/service-error-classification": "^4.2.7", "@smithy/smithy-client": "^4.10.3", "@smithy/types": "^4.11.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-E5hulijA59nBk/zvcwVMaS7FG7Y4l6hWA9vrW018r+8kiZef4/ETQaPI4oY+3zsy9f6KqDv3c4VKtO4DwwgpCg=="], "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w=="], @@ -907,7 +938,7 @@ "@smithy/signature-v4": ["@smithy/signature-v4@5.3.7", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.10.2", "", { "dependencies": { "@smithy/core": "^3.20.0", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-stack": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.10.3", "", { "dependencies": { "@smithy/core": "^3.20.1", "@smithy/middleware-endpoint": "^4.4.2", "@smithy/middleware-stack": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-EfECiO/0fAfb590LBnUe7rI5ux7XfquQ8LBzTe7gxw0j9QW/q8UT/EHWHlxV/+jhQ3+Ssga9uUYXCQgImGMbNg=="], "@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="], @@ -923,9 +954,9 @@ "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.16", "", { "dependencies": { "@smithy/property-provider": "^4.2.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.17", "", { "dependencies": { "@smithy/property-provider": "^4.2.7", "@smithy/smithy-client": "^4.10.3", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-dwN4GmivYF1QphnP3xJESXKtHvkkvKHSZI8GrSKMVoENVSKW2cFPRYC4ZgstYjUHdR3zwaDkIaTDIp26JuY7Cw=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.19", "", { "dependencies": { "@smithy/config-resolver": "^4.4.5", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.20", "", { "dependencies": { "@smithy/config-resolver": "^4.4.5", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/smithy-client": "^4.10.3", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-VD/I4AEhF1lpB3B//pmOIMBNLMrtdMXwy9yCOfa2QkJGDr63vH3RqPbSAKzoGMov3iryCxTXCxSsyGmEB8PDpg=="], "@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.7", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg=="], @@ -1143,6 +1174,22 @@ "@vercel/routing-utils": ["@vercel/routing-utils@5.3.1", "", { "dependencies": { "path-to-regexp": "6.1.0", "path-to-regexp-updated": "npm:path-to-regexp@6.3.0" }, "optionalDependencies": { "ajv": "^6.12.3" } }, "sha512-HlqFRdB6Dm20xgEWtEatchf9X28NifweXPdDoEGyj5ItngaiqpywtkgkuiAk3xK9eAu2oXM36wEJbDDTxMblUg=="], + "@vitest/coverage-v8": ["@vitest/coverage-v8@2.1.9", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", "debug": "^4.3.7", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.12", "magicast": "^0.3.5", "std-env": "^3.8.0", "test-exclude": "^7.0.1", "tinyrainbow": "^1.2.0" }, "peerDependencies": { "@vitest/browser": "2.1.9", "vitest": "2.1.9" }, "optionalPeers": ["@vitest/browser"] }, "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ=="], + + "@vitest/expect": ["@vitest/expect@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw=="], + + "@vitest/mocker": ["@vitest/mocker@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.12" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@2.1.9", "", { "dependencies": { "tinyrainbow": "^1.2.0" } }, "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ=="], + + "@vitest/runner": ["@vitest/runner@2.1.9", "", { "dependencies": { "@vitest/utils": "2.1.9", "pathe": "^1.1.2" } }, "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g=="], + + "@vitest/snapshot": ["@vitest/snapshot@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "magic-string": "^0.30.12", "pathe": "^1.1.2" } }, "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ=="], + + "@vitest/spy": ["@vitest/spy@2.1.9", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ=="], + + "@vitest/utils": ["@vitest/utils@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ=="], + "@volar/kit": ["@volar/kit@2.4.27", "", { "dependencies": { "@volar/language-service": "2.4.27", "@volar/typescript": "2.4.27", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-ilZoQDMLzqmSsImJRWx4YiZ4FcvvPrPnFVmL6hSsIWB6Bn3qc7k88J9yP32dagrs5Y8EXIlvvD/mAFaiuEOACQ=="], "@volar/language-core": ["@volar/language-core@2.4.27", "", { "dependencies": { "@volar/source-map": "2.4.27" } }, "sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ=="], @@ -1215,6 +1262,8 @@ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], @@ -1251,7 +1300,7 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.13", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-WhtvB2NG2wjr04+h77sg3klAIwrgOqnjS49GGudnUPGFFgg7G17y7Qecqp+2Dr5kUDxNRBca0SK7cG8JwzkWDQ=="], "better-auth": ["better-auth@1.4.10", "", { "dependencies": { "@better-auth/core": "1.4.10", "@better-auth/telemetry": "1.4.10", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.7", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-0kqwEBJLe8eyFzbUspRG/htOriCf9uMLlnpe34dlIJGdmDfPuQISd4shShvUrvIVhPxsY1dSTXdXPLpqISYOYg=="], @@ -1301,10 +1350,12 @@ "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001762", "", {}, "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw=="], + "caniuse-lite": ["caniuse-lite@1.0.30001763", "", {}, "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], @@ -1315,6 +1366,8 @@ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], @@ -1429,6 +1482,8 @@ "decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="], + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], @@ -1621,6 +1676,8 @@ "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + "ext": ["ext@1.7.0", "", { "dependencies": { "type": "^2.7.2" } }, "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -1901,6 +1958,14 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-lib-source-maps": ["istanbul-lib-source-maps@5.0.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0" } }, "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], @@ -1999,6 +2064,8 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], "lru-queue": ["lru-queue@0.1.0", "", { "dependencies": { "es5-ext": "~0.10.2" } }, "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ=="], @@ -2009,6 +2076,8 @@ "magicast": ["magicast@0.5.1", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "source-map-js": "^1.2.1" } }, "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw=="], + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], @@ -2275,7 +2344,9 @@ "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], "peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="], @@ -2291,6 +2362,10 @@ "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="], + + "playwright-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="], + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], @@ -2357,7 +2432,7 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-resizable-panels": ["react-resizable-panels@4.3.0", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-F8LGxzK3tq5N4m1oSvTZSDTH+QUhr5qPLuFhPyxVrOPqCFGZ1j2KOSx2K96aiKC4UI4kGR3/NKHczCumTgxVxA=="], + "react-resizable-panels": ["react-resizable-panels@4.3.1", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-Y+kWgV80snvOvSmWf5ivndOaaxRjaEz/b1snYJaPASqnFzYdfRd6eAXwPPKWtOm/96DNeDGdFssYR8pA8U/zRg=="], "react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="], @@ -2463,7 +2538,7 @@ "sass-formatter": ["sass-formatter@0.7.9", "", { "dependencies": { "suf-log": "^2.5.3" } }, "sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw=="], - "sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="], + "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], @@ -2497,6 +2572,8 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], @@ -2525,6 +2602,10 @@ "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], "stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="], @@ -2597,6 +2678,8 @@ "tar": ["tar@7.5.2", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg=="], + "test-exclude": ["test-exclude@7.0.1", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^9.0.4" } }, "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg=="], + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], @@ -2611,10 +2694,18 @@ "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@1.2.0", "", {}, "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ=="], + + "tinyspy": ["tinyspy@3.0.2", "", {}, "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], @@ -2749,8 +2840,12 @@ "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + "vite-node": ["vite-node@2.1.9", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.7", "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA=="], + "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], + "vitest": ["vitest@2.1.9", "", { "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", "@vitest/pretty-format": "^2.1.9", "@vitest/runner": "2.1.9", "@vitest/snapshot": "2.1.9", "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "debug": "^4.3.7", "expect-type": "^1.1.0", "magic-string": "^0.30.12", "pathe": "^1.1.2", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.1", "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", "vite-node": "2.1.9", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "2.1.9", "@vitest/ui": "2.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q=="], + "volar-service-css": ["volar-service-css@0.0.67", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-zV7C6enn9T9tuvQ6iSUyYEs34iPXR69Pf9YYWpbFYPWzVs22w96BtE8p04XYXbmjU6unt5oFt+iLL77bMB5fhA=="], "volar-service-emmet": ["volar-service-emmet@0.0.67", "", { "dependencies": { "@emmetio/css-parser": "^0.4.1", "@emmetio/html-matcher": "^1.3.0", "@vscode/emmet-helper": "^2.9.3", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-UDBL5x7KptmuJZNCCXMlCndMhFult/tj+9jXq3FH1ZGS1E4M/1U5hC06pg1c6e4kn+vnR6bqmvX0vIhL4f98+A=="], @@ -2807,6 +2902,8 @@ "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], @@ -2965,6 +3062,8 @@ "@vercel/nft/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@vitest/coverage-v8/magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], + "ajv-formats/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -3047,6 +3146,8 @@ "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "istanbul-reports/html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -3055,6 +3156,8 @@ "minio/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -3065,6 +3168,10 @@ "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], @@ -3087,10 +3194,14 @@ "tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + "tailwindcss/postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="], + "tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "test-exclude/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "tsup/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "typescript-eslint/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.52.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/type-utils": "8.52.0", "@typescript-eslint/utils": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.52.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q=="], @@ -3105,6 +3216,10 @@ "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "vite-node/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "vitest/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + "vscode-json-languageservice/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], "widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -3391,6 +3506,8 @@ "tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "test-exclude/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="], "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ=="], @@ -3419,6 +3536,8 @@ "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], + "vite-node/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], @@ -3471,6 +3590,8 @@ "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "vitest/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "widest-line/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "widest-line/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], @@ -3515,6 +3636,98 @@ "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="], + "vite-node/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "vite-node/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "vite-node/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "vite-node/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "vite-node/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "vite-node/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "vite-node/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "vite-node/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "vite-node/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "vite-node/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "vite-node/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "vite-node/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "vite-node/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "vite-node/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "vite-node/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "vite-node/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "vite-node/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "vite-node/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "vite-node/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "vite-node/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "vite-node/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "vite-node/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "vite-node/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + "widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "@astrojs/markdown-remark/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], diff --git a/package.json b/package.json index 6e44837..a40a2a8 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "workspaces": [ "apps/*", "packages/*", - "packages/config/*" + "packages/config/*", + "tests" ], "scripts": { "app": "bun run dev --filter=web", @@ -18,14 +19,21 @@ "format:check": "prettier \"**/*\" --ignore-unknown --list-different", "lint": "eslint . --cache --max-warnings 0", "reset:changelog": "rimraf --glob **/*.mdx", + "test": "turbo run test", + "test:coverage": "turbo run test:coverage", + "test:e2e": "turbo run test:e2e", + "tests:ui": "bun run --cwd=tests test:e2e:ui", "typecheck": "tsc --noEmit --tsBuildInfoFile .tsbuildinfo" }, "devDependencies": { "@formbase/tsconfig": "workspace:*", "@ianvs/prettier-plugin-sort-imports": "^4.2.1", "@types/eslint": "^8.56.10", + "@typescript-eslint/eslint-plugin": "^7.10.0", + "@typescript-eslint/parser": "^7.10.0", "eslint": "^8.57.0", "eslint-config-formbase": "workspace:*", + "eslint-plugin-eslint-comments": "^3.2.0", "prettier": "^3.2.5", "prettier-plugin-astro": "^0.13.0", "prettier-plugin-curly": "^0.2.1", diff --git a/packages/config/eslint/package.json b/packages/config/eslint/package.json index 6eeefdd..e814334 100644 --- a/packages/config/eslint/package.json +++ b/packages/config/eslint/package.json @@ -11,10 +11,12 @@ "lint": "eslint . --cache --max-warnings 0", "typecheck": "tsc --noEmit --tsBuildInfoFile .tsbuildinfo" }, + "dependencies": { + "@typescript-eslint/eslint-plugin": "^7.10.0", + "@typescript-eslint/parser": "^7.10.0" + }, "devDependencies": { "@formbase/tsconfig": "workspace:*", - "@typescript-eslint/eslint-plugin": "^7.10.0", - "@typescript-eslint/parser": "^7.10.0", "astro-eslint-parser": "^1.0.2", "eslint-config-next": "^16.1.1", "eslint-config-prettier": "^9.1.0", diff --git a/packages/email/index.ts b/packages/email/index.ts index ad6527d..34d2f13 100644 --- a/packages/email/index.ts +++ b/packages/email/index.ts @@ -41,6 +41,18 @@ let cachedTransporter: ReturnType | null = null; const getTransporter = () => { if (cachedTransporter) return cachedTransporter; + // Use noop transport in test environment + if (env.NODE_ENV === 'test') { + cachedTransporter = createTransport({ + name: 'noop', + version: '1.0.0', + send: (_mail, callback) => { + callback(null, { messageId: 'test-message-id' }); + }, + }); + return cachedTransporter; + } + if (env.SMTP_TRANSPORT === 'resend') { if (!env.RESEND_API_KEY) { throw new Error('Missing RESEND_API_KEY'); diff --git a/tests/.eslintrc.cjs b/tests/.eslintrc.cjs new file mode 100644 index 0000000..94a506c --- /dev/null +++ b/tests/.eslintrc.cjs @@ -0,0 +1,18 @@ +/** @type {import('eslint').Linter.Config} */ +const config = { + extends: ['formbase/base'], + rules: { + // Relax strict typing rules for test files + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unnecessary-condition': 'off', + '@typescript-eslint/require-await': 'off', + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/use-unknown-in-catch-callback-variable': 'off', + }, +}; + +module.exports = config; diff --git a/tests/api/form.test.ts b/tests/api/form.test.ts new file mode 100644 index 0000000..cb9fdf9 --- /dev/null +++ b/tests/api/form.test.ts @@ -0,0 +1,98 @@ +import { beforeEach, describe, expect, it } from 'vitest'; + +import { + createAuthenticatedCaller, + createTestSession, + createTestUser, + type TestSession, + type TestUser, +} from '../helpers'; + +describe('Form API', () => { + let user: TestUser; + let session: TestSession; + let caller: Awaited>; + + beforeEach(async () => { + const password = 'Password123!'; + user = await createTestUser({ + email: 'formtest@example.com', + password, + }); + session = await createTestSession(user.email, password); + caller = await createAuthenticatedCaller(user, session); + }); + + describe('form.create', () => { + it('creates a form with valid input and returns the form ID', async () => { + const result = await caller.form.create({ + title: 'Contact Form', + description: 'A contact form for the website', + returnUrl: 'https://example.com/thank-you', + }); + + expect(result).toHaveProperty('id'); + expect(result.id).toHaveLength(15); + }); + }); + + describe('form.get', () => { + it('retrieves a form by ID for the owner', async () => { + const created = await caller.form.create({ title: 'My Form' }); + const form = await caller.form.get({ formId: created.id }); + + expect(form).not.toBeNull(); + expect(form?.title).toBe('My Form'); + expect(form?.keys).toEqual(['']); + }); + + it('returns null for nonexistent form', async () => { + const form = await caller.form.get({ formId: 'nonexistent12345' }); + expect(form).toBeNull(); + }); + }); + + describe('form.update', () => { + it('updates form properties for the owner', async () => { + const created = await caller.form.create({ title: 'Original Title' }); + + await caller.form.update({ + id: created.id, + title: 'Updated Title', + description: 'New description', + enableSubmissions: false, + }); + + const form = await caller.form.get({ formId: created.id }); + expect(form?.title).toBe('Updated Title'); + expect(form?.description).toBe('New description'); + }); + }); + + describe('form.delete', () => { + it('deletes a form owned by the user', async () => { + const created = await caller.form.create({ title: 'To Delete' }); + await caller.form.delete({ id: created.id }); + + const form = await caller.form.get({ formId: created.id }); + expect(form).toBeNull(); + }); + }); + + describe('form.list', () => { + it('lists forms belonging to the user', async () => { + await caller.form.create({ title: 'Form 1' }); + await caller.form.create({ title: 'Form 2' }); + await caller.form.create({ title: 'Form 3' }); + + const result = await caller.form.list({ page: 1, perPage: 10 }); + + expect(result).toHaveLength(3); + // Check all forms exist (order may vary due to same-millisecond creation) + const titles = result.map((f) => f.title); + expect(titles).toContain('Form 1'); + expect(titles).toContain('Form 2'); + expect(titles).toContain('Form 3'); + }); + }); +}); diff --git a/tests/api/formData.test.ts b/tests/api/formData.test.ts new file mode 100644 index 0000000..25898f1 --- /dev/null +++ b/tests/api/formData.test.ts @@ -0,0 +1,81 @@ +import { beforeEach, describe, expect, it } from 'vitest'; + +import { + createAuthenticatedCaller, + createTestForm, + createTestSession, + createTestUser, + type TestForm, + type TestSession, + type TestUser, +} from '../helpers'; + +describe('FormData API', () => { + let user: TestUser; + let session: TestSession; + let caller: Awaited>; + let testForm: TestForm; + + beforeEach(async () => { + const password = 'Password123!'; + user = await createTestUser({ + email: 'formdata@example.com', + password, + }); + session = await createTestSession(user.email, password); + caller = await createAuthenticatedCaller(user, session); + testForm = await createTestForm({ userId: user.id, title: 'Test Form' }); + }); + + describe('formData.create', () => { + it('creates a submission for an owned form', async () => { + const result = await caller.formData.create({ + formId: testForm.id, + data: { name: 'John Doe', email: 'john@example.com' }, + }); + + expect(result).toHaveProperty('id'); + expect(result.id).toHaveLength(15); + }); + }); + + describe('formData.get', () => { + it('retrieves a submission by ID', async () => { + const created = await caller.formData.create({ + formId: testForm.id, + data: { name: 'Jane Doe' }, + }); + + const submission = await caller.formData.get({ id: created.id }); + + expect(submission).not.toBeNull(); + expect(submission?.data).toEqual({ name: 'Jane Doe' }); + }); + }); + + describe('formData.delete', () => { + it('deletes a submission', async () => { + const created = await caller.formData.create({ + formId: testForm.id, + data: { message: 'To delete' }, + }); + + await caller.formData.delete({ id: created.id }); + + // Getting a deleted submission should throw NOT_FOUND + await expect(caller.formData.get({ id: created.id })).rejects.toThrow(); + }); + }); + + describe('formData.all', () => { + it('lists all submissions for a form', async () => { + await caller.formData.create({ formId: testForm.id, data: { n: 1 } }); + await caller.formData.create({ formId: testForm.id, data: { n: 2 } }); + await caller.formData.create({ formId: testForm.id, data: { n: 3 } }); + + const submissions = await caller.formData.all({ formId: testForm.id }); + + expect(submissions).toHaveLength(3); + }); + }); +}); diff --git a/tests/api/security.test.ts b/tests/api/security.test.ts new file mode 100644 index 0000000..8084ed4 --- /dev/null +++ b/tests/api/security.test.ts @@ -0,0 +1,220 @@ +import { beforeEach, describe, expect, it } from 'vitest'; + +import { + createAuthenticatedCaller, + createTestForm, + createTestFormData, + createTestSession, + createTestUser, + createUnauthenticatedCaller, + type TestForm, + type TestFormData, + type TestSession, + type TestUser, +} from '../helpers'; + +describe('Security Matrix', () => { + // Test users + let userA: TestUser; + let userB: TestUser; + let sessionA: TestSession; + let sessionB: TestSession; + let callerA: Awaited>; + let callerB: Awaited>; + let unauthCaller: Awaited>; + + // Test data + let formA: TestForm; + let submissionA: TestFormData; + + beforeEach(async () => { + // Create two test users + const passwordA = 'PasswordA123!'; + const passwordB = 'PasswordB123!'; + + userA = await createTestUser({ + email: 'usera@example.com', + password: passwordA, + }); + userB = await createTestUser({ + email: 'userb@example.com', + password: passwordB, + }); + + sessionA = await createTestSession(userA.email, passwordA); + sessionB = await createTestSession(userB.email, passwordB); + + callerA = await createAuthenticatedCaller(userA, sessionA); + callerB = await createAuthenticatedCaller(userB, sessionB); + unauthCaller = await createUnauthenticatedCaller(); + + // Create test data owned by User A + formA = await createTestForm({ + userId: userA.id, + title: 'User A Form', + }); + submissionA = await createTestFormData({ + formId: formA.id, + data: { secret: 'data' }, + }); + }); + + describe('Cross-user form isolation', () => { + it('User B cannot read User A form via form.get', async () => { + const result = await callerB.form.get({ formId: formA.id }); + expect(result).toBeNull(); + }); + + it('User B cannot update User A form', async () => { + await expect( + callerB.form.update({ id: formA.id, title: 'Hacked' }), + ).rejects.toThrow(); + }); + + it('User B cannot delete User A form', async () => { + await expect(callerB.form.delete({ id: formA.id })).rejects.toThrow(); + }); + + it("User B form list does not include User A forms", async () => { + const formB = await createTestForm({ + userId: userB.id, + title: 'User B Form', + }); + const listB = await callerB.form.list({ page: 1, perPage: 10 }); + + expect(listB.map((f) => f.id)).not.toContain(formA.id); + expect(listB.map((f) => f.id)).toContain(formB.id); + }); + }); + + describe('Cross-user submission isolation', () => { + it('User B cannot read User A submissions via formData.get', async () => { + await expect( + callerB.formData.get({ id: submissionA.id }), + ).rejects.toThrow(); + }); + + it('User B cannot delete User A submissions', async () => { + await expect( + callerB.formData.delete({ id: submissionA.id }), + ).rejects.toThrow(); + }); + + it('User B cannot list User A form submissions', async () => { + await expect( + callerB.formData.all({ formId: formA.id }), + ).rejects.toThrow(); + }); + + it('User B cannot create submission on User A form via protected route', async () => { + await expect( + callerB.formData.create({ + formId: formA.id, + data: { attack: 'data' }, + }), + ).rejects.toThrow(); + }); + }); + + describe('Unauthenticated access attempts', () => { + it('form.create requires authentication', async () => { + await expect( + unauthCaller.form.create({ title: 'Anon Form' }), + ).rejects.toThrow('UNAUTHORIZED'); + }); + + it('form.get requires authentication', async () => { + await expect( + unauthCaller.form.get({ formId: formA.id }), + ).rejects.toThrow('UNAUTHORIZED'); + }); + + it('form.update requires authentication', async () => { + await expect( + unauthCaller.form.update({ id: formA.id, title: 'X' }), + ).rejects.toThrow('UNAUTHORIZED'); + }); + + it('form.delete requires authentication', async () => { + await expect( + unauthCaller.form.delete({ id: formA.id }), + ).rejects.toThrow('UNAUTHORIZED'); + }); + + it('form.list requires authentication', async () => { + await expect( + unauthCaller.form.list({ page: 1, perPage: 10 }), + ).rejects.toThrow('UNAUTHORIZED'); + }); + + it('formData.create requires authentication', async () => { + await expect( + unauthCaller.formData.create({ formId: formA.id, data: {} }), + ).rejects.toThrow('UNAUTHORIZED'); + }); + + it('formData.get requires authentication', async () => { + await expect( + unauthCaller.formData.get({ id: submissionA.id }), + ).rejects.toThrow('UNAUTHORIZED'); + }); + + it('formData.delete requires authentication', async () => { + await expect( + unauthCaller.formData.delete({ id: submissionA.id }), + ).rejects.toThrow('UNAUTHORIZED'); + }); + + it('formData.all requires authentication', async () => { + await expect( + unauthCaller.formData.all({ formId: formA.id }), + ).rejects.toThrow('UNAUTHORIZED'); + }); + + it('user.get requires authentication', async () => { + await expect(unauthCaller.user.get()).rejects.toThrow('UNAUTHORIZED'); + }); + + it('user.update requires authentication', async () => { + await expect( + unauthCaller.user.update({ id: 'x', name: 'X' }), + ).rejects.toThrow('UNAUTHORIZED'); + }); + }); + + describe('Deleted resource handling', () => { + it('accessing deleted form returns null', async () => { + const tempForm = await createTestForm({ + userId: userA.id, + title: 'Temp', + }); + await callerA.form.delete({ id: tempForm.id }); + + const result = await callerA.form.get({ formId: tempForm.id }); + expect(result).toBeNull(); + }); + + it('updating deleted form throws NOT_FOUND', async () => { + const tempForm = await createTestForm({ + userId: userA.id, + title: 'Temp', + }); + await callerA.form.delete({ id: tempForm.id }); + + await expect( + callerA.form.update({ id: tempForm.id, title: 'X' }), + ).rejects.toThrow(); + }); + + it('accessing nonexistent form ID returns null', async () => { + const result = await callerA.form.get({ formId: 'nonexistent12345' }); + expect(result).toBeNull(); + }); + + it('accessing nonexistent submission ID throws NOT_FOUND', async () => { + await expect( + callerA.formData.get({ id: 'nonexistent12345' }), + ).rejects.toThrow(); + }); + }); +}); diff --git a/tests/api/user.test.ts b/tests/api/user.test.ts new file mode 100644 index 0000000..06c7f1a --- /dev/null +++ b/tests/api/user.test.ts @@ -0,0 +1,58 @@ +import { beforeEach, describe, expect, it } from 'vitest'; + +import { + createAuthenticatedCaller, + createTestSession, + createTestUser, + getTestDb, + type TestSession, + type TestUser, +} from '../helpers'; + +describe('User API', () => { + let user: TestUser; + let session: TestSession; + let caller: Awaited>; + + beforeEach(async () => { + const password = 'Password123!'; + user = await createTestUser({ + email: 'usertest@example.com', + name: 'Test User', + password, + }); + session = await createTestSession(user.email, password); + caller = await createAuthenticatedCaller(user, session); + }); + + describe('user.get', () => { + it('returns the current authenticated user', async () => { + const result = await caller.user.get(); + + expect(result).toEqual( + expect.objectContaining({ + id: user.id, + email: user.email, + name: user.name, + }), + ); + }); + }); + + describe('user.update', () => { + it('updates the user name in the database', async () => { + await caller.user.update({ + id: user.id, + name: 'Updated Name', + }); + + // Verify the database was updated directly + // (user.get returns cached ctx.user, so we check the DB) + const db = getTestDb(); + const updated = await db.query.users.findFirst({ + where: (table, { eq }) => eq(table.id, user.id), + }); + expect(updated?.name).toBe('Updated Name'); + }); + }); +}); diff --git a/tests/e2e/auth.spec.ts b/tests/e2e/auth.spec.ts new file mode 100644 index 0000000..a764889 --- /dev/null +++ b/tests/e2e/auth.spec.ts @@ -0,0 +1,112 @@ +import { expect, test } from '@playwright/test'; + +import { E2E_TEST_USER } from './seed'; + +test.describe('Authentication', () => { + test.describe('Login Flow', () => { + test('displays login form correctly', async ({ page }) => { + await page.goto('/login'); + + // Check page title and description + await expect(page.getByText('Formbase Log In')).toBeVisible(); + await expect( + page.getByText('Log in to your account to access your dashboard'), + ).toBeVisible(); + + // Check form elements + await expect(page.getByPlaceholder('email@example.com')).toBeVisible(); + await expect(page.getByPlaceholder('********')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Log In', exact: true })).toBeVisible(); + }); + + test('shows error for invalid credentials', async ({ page }) => { + await page.goto('/login'); + + await page.getByPlaceholder('email@example.com').fill('invalid@example.com'); + await page.getByPlaceholder('********').fill('wrongpassword'); + await page.getByRole('button', { name: 'Log In', exact: true }).click(); + + // Wait for error message + await expect(page.getByText(/invalid|error|incorrect/i)).toBeVisible({ + timeout: 10000, + }); + }); + + test('successfully logs in with valid credentials', async ({ page }) => { + await page.goto('/login'); + + await page.getByPlaceholder('email@example.com').fill(E2E_TEST_USER.email); + await page.getByPlaceholder('********').fill(E2E_TEST_USER.password); + await page.getByRole('button', { name: 'Log In', exact: true }).click(); + + // Should redirect to dashboard + await page.waitForURL(/.*dashboard/, { timeout: 15000 }); + }); + + test('redirects authenticated user from login to dashboard', async ({ + page, + }) => { + // First, log in + await page.goto('/login'); + await page.getByPlaceholder('email@example.com').fill(E2E_TEST_USER.email); + await page.getByPlaceholder('********').fill(E2E_TEST_USER.password); + await page.getByRole('button', { name: 'Log In', exact: true }).click(); + await page.waitForURL(/.*dashboard/, { timeout: 15000 }); + + // Now try to visit login page again + await page.goto('/login'); + + // Should redirect back to dashboard (or stay on protected page) + await expect(page).toHaveURL(/.*dashboard/, { timeout: 10000 }); + }); + }); + + test.describe('Logout Flow', () => { + test('successfully logs out', async ({ page }) => { + // First, log in + await page.goto('/login'); + await page.getByPlaceholder('email@example.com').fill(E2E_TEST_USER.email); + await page.getByPlaceholder('********').fill(E2E_TEST_USER.password); + await page.getByRole('button', { name: 'Log In', exact: true }).click(); + await page.waitForURL(/.*dashboard/, { timeout: 15000 }); + + // Open user menu dropdown (avatar button in header showing user initials) + const userMenuButton = page.locator('header button').last(); + await userMenuButton.click(); + + // Click logout in the dropdown menu (it's a button, not menuitem) + await page.getByRole('button', { name: /sign\s*out/i }).click(); + + // Confirm the sign out dialog + await page.getByRole('button', { name: 'Continue' }).click(); + + // After logout, should be on home or login page (root URL ends with port/) + await expect(page).toHaveURL(/:\d+\/?$|\/login/, { timeout: 10000 }); + }); + }); + + test.describe('Protected Routes', () => { + test('redirects unauthenticated user to login', async ({ page }) => { + // Clear any existing session + await page.context().clearCookies(); + + // Try to access dashboard directly + await page.goto('/dashboard'); + + // Should redirect to login + await expect(page).toHaveURL(/.*login/, { timeout: 10000 }); + }); + + test('allows authenticated user to access dashboard', async ({ page }) => { + // Log in first + await page.goto('/login'); + await page.getByPlaceholder('email@example.com').fill(E2E_TEST_USER.email); + await page.getByPlaceholder('********').fill(E2E_TEST_USER.password); + await page.getByRole('button', { name: 'Log In', exact: true }).click(); + await page.waitForURL(/.*dashboard/, { timeout: 15000 }); + + // Dashboard should show the main heading + await expect(page.getByRole('heading', { name: 'Form Endpoints' })).toBeVisible(); + }); + }); +}); diff --git a/tests/e2e/cleanup.ts b/tests/e2e/cleanup.ts new file mode 100644 index 0000000..ae7a404 --- /dev/null +++ b/tests/e2e/cleanup.ts @@ -0,0 +1,11 @@ +import { cleanupE2EData } from './seed'; + +cleanupE2EData() + .then(() => { + console.log('🎉 E2E cleanup complete'); + process.exit(0); + }) + .catch((error) => { + console.error('❌ E2E cleanup failed:', error); + process.exit(1); + }); diff --git a/tests/e2e/forms.spec.ts b/tests/e2e/forms.spec.ts new file mode 100644 index 0000000..34c91a3 --- /dev/null +++ b/tests/e2e/forms.spec.ts @@ -0,0 +1,119 @@ +import { expect, test } from '@playwright/test'; + +import { E2E_TEST_USER } from './seed'; + +test.describe('Forms', () => { + // Login before each test in this suite + test.beforeEach(async ({ page }) => { + await page.goto('/login'); + await page.getByPlaceholder('email@example.com').fill(E2E_TEST_USER.email); + await page.getByPlaceholder('********').fill(E2E_TEST_USER.password); + await page.getByRole('button', { name: 'Log In', exact: true }).click(); + await page.waitForURL(/.*dashboard/, { timeout: 15000 }); + }); + + test.describe('Form Creation', () => { + test('displays create form dialog', async ({ page }) => { + // Wait for page to be fully loaded before interacting + await page.waitForLoadState('networkidle'); + + // Click create form button + await page.getByRole('button', { name: 'New Form Endpoint' }).click(); + + // Dialog should appear - wait for the dialog title (more reliable with portals) + await expect( + page.getByRole('heading', { name: 'Create New Form Endpoint' }), + ).toBeVisible({ timeout: 5000 }); + + // Dialog should have the Name input field + await expect(page.getByRole('textbox', { name: 'Name' })).toBeVisible(); + }); + + test('creates a new form', async ({ page }) => { + // Wait for page to be fully loaded before interacting + await page.waitForLoadState('networkidle'); + + // Click create button + await page.getByRole('button', { name: 'New Form Endpoint' }).click(); + + // Wait for dialog title to appear (more reliable with portals) + await expect( + page.getByRole('heading', { name: 'Create New Form Endpoint' }), + ).toBeVisible({ timeout: 5000 }); + + // Use unique form name to avoid strict mode violation from previous test runs + const uniqueFormName = `E2E Test ${Date.now()}`; + + // Fill in form name + await page.getByRole('textbox', { name: 'Name' }).fill(uniqueFormName); + + // Submit the dialog + await page.getByRole('button', { name: 'Create Form' }).click(); + + // Should see the new form in the list or be redirected to it + await expect(page.getByText(uniqueFormName)).toBeVisible({ + timeout: 10000, + }); + }); + }); + + test.describe('Form List', () => { + test('displays user forms on dashboard', async ({ page }) => { + // Dashboard should show at least the seeded form + await expect(page.getByText('E2E Test Form')).toBeVisible({ + timeout: 10000, + }); + }); + + test('can navigate to form details', async ({ page }) => { + // Click on the test form + await page.getByText('E2E Test Form').click(); + + // Should navigate to form detail page + await expect(page).toHaveURL(/.*form.*/, { timeout: 10000 }); + }); + }); + + test.describe('Form Settings', () => { + test('can view and update form settings', async ({ page }) => { + // Navigate to form + await page.getByText('E2E Test Form').click(); + await expect(page).toHaveURL(/.*form.*/, { timeout: 10000 }); + + // Look for settings tab or button + const settingsTab = page.getByRole('tab', { name: /settings/i }); + const settingsButton = page.getByRole('button', { name: /settings/i }); + const settingsLink = page.getByRole('link', { name: /settings/i }); + + if (await settingsTab.isVisible()) { + await settingsTab.click(); + } else if (await settingsButton.isVisible()) { + await settingsButton.click(); + } else if (await settingsLink.isVisible()) { + await settingsLink.click(); + } + + // Settings should show form configuration options + const titleInput = page.getByLabel(/title/i); + if (await titleInput.isVisible()) { + await expect(titleInput).toHaveValue('E2E Test Form'); + } + }); + }); + + test.describe('Form Submissions', () => { + test('can view submissions for a form', async ({ page }) => { + // Navigate to form + await page.getByText('E2E Test Form').click(); + await expect(page).toHaveURL(/.*form.*/, { timeout: 10000 }); + + // Click Submissions tab + await page.getByRole('tab', { name: 'Submissions' }).click(); + + // Should see submissions panel (empty state shows "No Submissions Available") + await expect( + page.getByRole('heading', { name: 'No Submissions Available' }), + ).toBeVisible({ timeout: 5000 }); + }); + }); +}); diff --git a/tests/e2e/seed.ts b/tests/e2e/seed.ts new file mode 100644 index 0000000..035810f --- /dev/null +++ b/tests/e2e/seed.ts @@ -0,0 +1,127 @@ +async function loadDbDeps() { + const [{ db, drizzlePrimitives }, schema] = await Promise.all([ + import('@formbase/db'), + import('@formbase/db/schema'), + ]); + + return { + db, + eq: drizzlePrimitives.eq, + accounts: schema.accounts, + forms: schema.forms, + sessions: schema.sessions, + users: schema.users, + }; +} + +// Test user credentials (used in E2E tests) +export const E2E_TEST_USER = { + email: 'e2e-test@formbase.dev', + password: 'TestPassword123!', + name: 'E2E Test User', +}; + +export async function seedE2EData() { + console.log('🌱 Seeding E2E test data...'); + const { db, eq, forms, users } = await loadDbDeps(); + const { generateId } = await import('@formbase/utils/generate-id'); + const { auth } = await import('@formbase/auth'); + + // Check if test user already exists + const existingUser = await db.query.users.findFirst({ + where: (table) => eq(table.email, E2E_TEST_USER.email), + }); + + let userId: string; + + if (existingUser) { + console.log('✅ E2E test user already exists'); + userId = existingUser.id; + } else { + // Use better-auth's signUp API to create user with properly hashed password + const response = await auth.api.signUpEmail({ + body: { + email: E2E_TEST_USER.email, + password: E2E_TEST_USER.password, + name: E2E_TEST_USER.name, + }, + }); + + if (!response.user) { + throw new Error('Failed to create E2E test user'); + } + + userId = response.user.id; + + // Mark email as verified so user can log in immediately + await db + .update(users) + .set({ emailVerified: true }) + .where(eq(users.id, userId)); + + console.log('✅ Created E2E test user:', E2E_TEST_USER.email); + } + + // Check if test form already exists + const existingForm = await db.query.forms.findFirst({ + where: (table) => eq(table.userId, userId), + }); + + if (existingForm) { + console.log('✅ E2E test form already exists'); + return { userId, formId: existingForm.id }; + } + + // Create a test form for the user + const formId = generateId(15); + await db.insert(forms).values({ + id: formId, + userId: userId, + title: 'E2E Test Form', + description: 'A form created for E2E testing', + enableSubmissions: true, + enableEmailNotifications: false, + keys: JSON.stringify(['']), + createdAt: new Date(), + updatedAt: new Date(), + }); + + console.log('✅ Created E2E test form:', formId); + + return { userId, formId }; +} + +export async function cleanupE2EData() { + console.log('🧹 Cleaning up E2E test data...'); + const { db, eq, accounts, forms, sessions, users } = await loadDbDeps(); + + const testUser = await db.query.users.findFirst({ + where: (table) => eq(table.email, E2E_TEST_USER.email), + }); + + if (!testUser) { + console.log('ℹ️ No E2E test data to clean up'); + return; + } + + // Delete in order due to foreign key constraints + await db.delete(sessions).where(eq(sessions.userId, testUser.id)); + await db.delete(forms).where(eq(forms.userId, testUser.id)); + await db.delete(accounts).where(eq(accounts.userId, testUser.id)); + await db.delete(users).where(eq(users.id, testUser.id)); + + console.log('✅ Cleaned up E2E test data'); +} + +// Run if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + seedE2EData() + .then(() => { + console.log('🎉 E2E seeding complete'); + process.exit(0); + }) + .catch((error) => { + console.error('❌ E2E seeding failed:', error); + process.exit(1); + }); +} diff --git a/tests/helpers/auth.ts b/tests/helpers/auth.ts new file mode 100644 index 0000000..a68e98c --- /dev/null +++ b/tests/helpers/auth.ts @@ -0,0 +1,89 @@ +import { users } from '@formbase/db/schema'; +import { generateId } from '@formbase/utils/generate-id'; + +export interface TestUser { + id: string; + email: string; + name: string; + emailVerified: boolean; +} + +export interface TestSession { + token: string; + userId: string; +} + +export async function createTestUser( + options: { + email?: string; + name?: string; + password?: string; + emailVerified?: boolean; + } = {}, +): Promise { + const { auth } = await import('@formbase/auth'); + const { db, drizzlePrimitives } = await import('@formbase/db'); + + const email = options.email ?? `test-${generateId(15)}@example.com`; + const name = options.name ?? 'Test User'; + const password = options.password ?? 'TestPassword123!'; + const emailVerified = options.emailVerified ?? true; + + // Use better-auth's signUp API to create user with properly hashed password + const response = await auth.api.signUpEmail({ + body: { + email, + password, + name, + }, + }); + + if (!response.user) { + throw new Error(`Failed to create test user: ${email}`); + } + + // Mark email as verified if needed + if (emailVerified) { + await db + .update(users) + .set({ emailVerified: true }) + .where(drizzlePrimitives.eq(users.id, response.user.id)); + } + + return { + id: response.user.id, + email: response.user.email, + name: response.user.name ?? name, + emailVerified, + }; +} + +export async function createTestSession( + email: string, + password: string, +): Promise { + const { auth } = await import('@formbase/auth'); + + // Use better-auth's signIn API to create a session + const response = await auth.api.signInEmail({ + body: { + email, + password, + }, + }); + + if (!response.token || !response.user) { + throw new Error(`Failed to create session for: ${email}`); + } + + return { + token: response.token, + userId: response.user.id, + }; +} + +export function createAuthHeaders(sessionToken: string): Headers { + const headers = new Headers(); + headers.set('Cookie', `better-auth.session_token=${sessionToken}`); + return headers; +} diff --git a/tests/helpers/db.ts b/tests/helpers/db.ts new file mode 100644 index 0000000..a44421b --- /dev/null +++ b/tests/helpers/db.ts @@ -0,0 +1,149 @@ +import { createClient, type Client } from '@libsql/client'; +import { drizzle, type LibSQLDatabase } from 'drizzle-orm/libsql'; + +import * as schema from '@formbase/db/schema'; + +let client: Client; +let testDb: LibSQLDatabase; + +const SCHEMA_SQL = ` +CREATE TABLE IF NOT EXISTS user ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT, + email TEXT NOT NULL UNIQUE, + email_verified INTEGER DEFAULT 0 NOT NULL, + image TEXT, + created_at INTEGER DEFAULT (cast(unixepoch('subsec') * 1000 as integer)) NOT NULL, + updated_at INTEGER DEFAULT (cast(unixepoch('subsec') * 1000 as integer)) NOT NULL +); + +CREATE TABLE IF NOT EXISTS session ( + id TEXT PRIMARY KEY NOT NULL, + expires_at INTEGER NOT NULL, + token TEXT NOT NULL UNIQUE, + created_at INTEGER DEFAULT (cast(unixepoch('subsec') * 1000 as integer)) NOT NULL, + updated_at INTEGER DEFAULT (cast(unixepoch('subsec') * 1000 as integer)) NOT NULL, + ip_address TEXT, + user_agent TEXT, + user_id TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS account ( + id TEXT PRIMARY KEY NOT NULL, + account_id TEXT NOT NULL, + provider_id TEXT NOT NULL, + user_id TEXT NOT NULL, + access_token TEXT, + refresh_token TEXT, + id_token TEXT, + access_token_expires_at INTEGER, + refresh_token_expires_at INTEGER, + scope TEXT, + password TEXT, + created_at INTEGER DEFAULT (cast(unixepoch('subsec') * 1000 as integer)) NOT NULL, + updated_at INTEGER DEFAULT (cast(unixepoch('subsec') * 1000 as integer)) NOT NULL, + FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS forms ( + id TEXT PRIMARY KEY NOT NULL, + user_id TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT, + created_at INTEGER DEFAULT (cast(unixepoch('subsec') * 1000 as integer)) NOT NULL, + updated_at INTEGER, + return_url TEXT, + send_email_for_new_submissions INTEGER DEFAULT 1 NOT NULL, + keys TEXT NOT NULL, + enable_submissions INTEGER DEFAULT 1 NOT NULL, + enable_retention INTEGER DEFAULT 1 NOT NULL, + default_submission_email TEXT, + FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS form_datas ( + id TEXT PRIMARY KEY NOT NULL, + form_id TEXT NOT NULL, + data TEXT NOT NULL, + created_at INTEGER DEFAULT (cast(unixepoch('subsec') * 1000 as integer)) NOT NULL, + FOREIGN KEY (form_id) REFERENCES forms(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS onboarding_forms ( + id TEXT PRIMARY KEY NOT NULL, + user_id TEXT NOT NULL, + form_id TEXT NOT NULL, + created_at INTEGER DEFAULT (cast(unixepoch('subsec') * 1000 as integer)) NOT NULL, + FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE, + FOREIGN KEY (form_id) REFERENCES forms(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS verification ( + id TEXT PRIMARY KEY NOT NULL, + identifier TEXT NOT NULL, + value TEXT NOT NULL, + expires_at INTEGER NOT NULL, + created_at INTEGER DEFAULT (cast(unixepoch('subsec') * 1000 as integer)) NOT NULL, + updated_at INTEGER DEFAULT (cast(unixepoch('subsec') * 1000 as integer)) NOT NULL +); + +CREATE INDEX IF NOT EXISTS user_email_idx ON user(email); +CREATE INDEX IF NOT EXISTS session_userId_idx ON session(user_id); +CREATE INDEX IF NOT EXISTS account_userId_idx ON account(user_id); +CREATE INDEX IF NOT EXISTS form_user_idx ON forms(user_id); +CREATE INDEX IF NOT EXISTS form_created_at_idx ON forms(created_at); +CREATE INDEX IF NOT EXISTS form_idx ON form_datas(form_id); +CREATE INDEX IF NOT EXISTS form_data_created_at_idx ON form_datas(created_at); +CREATE INDEX IF NOT EXISTS verification_identifier_idx ON verification(identifier); +`; + +const RESET_SQL = ` +DELETE FROM form_datas; +DELETE FROM onboarding_forms; +DELETE FROM forms; +DELETE FROM session; +DELETE FROM account; +DELETE FROM verification; +DELETE FROM user; +`; + +export async function setupTestDatabase(): Promise { + // Use shared in-memory database - matches DATABASE_URL env var set in vitest.setup.ts + client = createClient({ + url: 'file::memory:?cache=shared', + }); + testDb = drizzle(client, { schema }); + + // Create schema - execute each statement separately + const statements = SCHEMA_SQL.split(';') + .map((s) => s.trim()) + .filter((s) => s.length > 0); + + for (const stmt of statements) { + await client.execute(stmt); + } +} + +export async function resetDatabase(): Promise { + // Delete data in correct order to respect foreign keys + const statements = RESET_SQL.split(';') + .map((s) => s.trim()) + .filter((s) => s.length > 0); + + for (const stmt of statements) { + await client.execute(stmt); + } +} + +export async function teardownTestDatabase(): Promise { + client.close(); +} + +export function getTestDb(): LibSQLDatabase { + return testDb; +} + +export function getTestClient(): Client { + return client; +} diff --git a/tests/helpers/factories.ts b/tests/helpers/factories.ts new file mode 100644 index 0000000..7e7e12d --- /dev/null +++ b/tests/helpers/factories.ts @@ -0,0 +1,76 @@ +import { formDatas, forms } from '@formbase/db/schema'; +import { generateId } from '@formbase/utils/generate-id'; + +import { getTestDb } from './db'; + +export interface TestForm { + id: string; + userId: string; + title: string; + description: string | null; +} + +export interface TestFormData { + id: string; + formId: string; + data: Record; +} + +export async function createTestForm(options: { + userId: string; + title?: string; + description?: string; + enableSubmissions?: boolean; + enableEmailNotifications?: boolean; + returnUrl?: string; + keys?: string[]; +}): Promise { + const db = getTestDb(); + const formId = generateId(15); + const title = options.title ?? 'Test Form'; + const description = options.description ?? null; + + db.insert(forms) + .values({ + id: formId, + userId: options.userId, + title, + description, + keys: JSON.stringify(options.keys ?? ['']), + enableSubmissions: options.enableSubmissions ?? true, + enableEmailNotifications: options.enableEmailNotifications ?? false, + returnUrl: options.returnUrl ?? null, + updatedAt: new Date(), + }) + .run(); + + return { + id: formId, + userId: options.userId, + title, + description, + }; +} + +export async function createTestFormData(options: { + formId: string; + data?: Record; +}): Promise { + const db = getTestDb(); + const id = generateId(15); + const data = options.data ?? { name: 'Test', email: 'test@example.com' }; + + db.insert(formDatas) + .values({ + id, + formId: options.formId, + data: JSON.stringify(data), + }) + .run(); + + return { + id, + formId: options.formId, + data, + }; +} diff --git a/tests/helpers/index.ts b/tests/helpers/index.ts new file mode 100644 index 0000000..5956fd4 --- /dev/null +++ b/tests/helpers/index.ts @@ -0,0 +1,4 @@ +export * from './auth'; +export * from './db'; +export * from './factories'; +export * from './trpc'; diff --git a/tests/helpers/trpc.ts b/tests/helpers/trpc.ts new file mode 100644 index 0000000..c3e493e --- /dev/null +++ b/tests/helpers/trpc.ts @@ -0,0 +1,70 @@ +import { createCaller } from '@formbase/api'; + +import { type TestSession, type TestUser } from './auth'; +import { getTestDb } from './db'; + +export async function createTestCaller(options?: { + user?: TestUser; + session?: TestSession; +}) { + const db = getTestDb(); + const { user, session } = options ?? {}; + + if (!session || !user) { + return createCaller(() => ({ + db, + session: null, + user: null, + headers: new Headers(), + })); + } + + const now = new Date(); + const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); + + return createCaller(() => ({ + db, + session: { + session: { + id: `test-session-${session.userId}`, + token: session.token, + userId: session.userId, + expiresAt, + createdAt: now, + updatedAt: now, + ipAddress: null, + userAgent: null, + }, + user: { + id: user.id, + email: user.email, + name: user.name, + emailVerified: user.emailVerified, + image: null, + createdAt: now, + updatedAt: now, + }, + }, + user: { + id: user.id, + email: user.email, + name: user.name, + emailVerified: user.emailVerified, + image: null, + createdAt: now, + updatedAt: now, + }, + headers: new Headers(), + })); +} + +export async function createUnauthenticatedCaller() { + return createTestCaller(); +} + +export async function createAuthenticatedCaller( + user: TestUser, + session: TestSession, +) { + return createTestCaller({ user, session }); +} diff --git a/tests/package.json b/tests/package.json new file mode 100644 index 0000000..9e08424 --- /dev/null +++ b/tests/package.json @@ -0,0 +1,27 @@ +{ + "name": "@formbase/tests", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:seed": "bun run --env-file=../apps/web/.env.local ./e2e/seed.ts", + "test:e2e:cleanup": "bun run --env-file=../apps/web/.env.local ./e2e/cleanup.ts" + }, + "devDependencies": { + "@formbase/api": "workspace:*", + "@formbase/db": "workspace:*", + "@formbase/utils": "workspace:*", + "@libsql/client": "^0.14.0", + "@playwright/test": "^1.49.0", + "@types/node": "^20.17.0", + "@vitest/coverage-v8": "^2.1.8", + "drizzle-orm": "^0.30.10", + "typescript": "5.8.2", + "vitest": "^2.1.8" + } +} diff --git a/tests/playwright.config.ts b/tests/playwright.config.ts new file mode 100644 index 0000000..1d61fc2 --- /dev/null +++ b/tests/playwright.config.ts @@ -0,0 +1,32 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: false, // Serial for E2E with shared DB + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: [['html', { open: 'never' }], ['list']], + use: { + baseURL: process.env.E2E_BASE_URL ?? 'http://localhost:3000', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'on', + ignoreHTTPSErrors: true, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: process.env.CI + ? undefined + : { + command: 'bun run dev --filter=web', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + timeout: 120000, + cwd: '..', + }, +}); diff --git a/tests/routes/json-parsing.test.ts b/tests/routes/json-parsing.test.ts new file mode 100644 index 0000000..83e97ce --- /dev/null +++ b/tests/routes/json-parsing.test.ts @@ -0,0 +1,555 @@ +import type { TestForm, TestUser } from '../helpers'; + +import { beforeEach, describe, expect, it } from 'vitest'; + +import { + createTestForm, + createTestUser, + createUnauthenticatedCaller, + getTestDb, +} from '../helpers'; + +describe('JSON Parsing Edge Cases', () => { + let user: TestUser; + let testForm: TestForm; + let publicCaller: Awaited>; + + beforeEach(async () => { + user = await createTestUser({ + email: 'jsontest@example.com', + }); + testForm = await createTestForm({ + userId: user.id, + title: 'JSON Edge Case Form', + enableEmailNotifications: false, + }); + publicCaller = await createUnauthenticatedCaller(); + }); + + describe('Unicode field names', () => { + it('handles Japanese characters in field names', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + 名前: 'テスト', + メール: 'test@example.com', + }, + keys: ['名前', 'メール'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data['名前']).toBe('テスト'); + expect(data['メール']).toBe('test@example.com'); + }); + + it('handles Cyrillic characters in field names', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + Имя: 'Тест', + Электронная_почта: 'test@example.com', + }, + keys: ['Имя', 'Электронная_почта'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data['Имя']).toBe('Тест'); + }); + + it('handles emoji in field names', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + '👤_name': 'John', + '📧_email': 'john@example.com', + '💬_message': 'Hello! 🎉', + }, + keys: ['👤_name', '📧_email', '💬_message'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data['👤_name']).toBe('John'); + expect(data['💬_message']).toBe('Hello! 🎉'); + }); + + it('handles mixed Unicode scripts', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + name_名前_Имя: 'Mixed', + العربية: 'Arabic', + עברית: 'Hebrew', + }, + keys: ['name_名前_Имя', 'العربية', 'עברית'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data['name_名前_Имя']).toBe('Mixed'); + }); + }); + + describe('Empty structures', () => { + it('handles empty arrays', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + tags: [], + items: [], + }, + keys: ['tags', 'items'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data.tags).toEqual([]); + expect(data.items).toEqual([]); + }); + + it('handles empty objects', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + metadata: {}, + config: {}, + }, + keys: ['metadata', 'config'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data.metadata).toEqual({}); + expect(data.config).toEqual({}); + }); + + it('handles empty string values', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + name: '', + description: '', + }, + keys: ['name', 'description'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data.name).toBe(''); + expect(data.description).toBe(''); + }); + }); + + describe('Deeply nested structures', () => { + it('handles 10 levels of nesting', async () => { + const deeplyNested = { + level1: { + level2: { + level3: { + level4: { + level5: { + level6: { + level7: { + level8: { + level9: { + level10: 'deep value', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: deeplyNested, + keys: ['level1'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect( + data.level1.level2.level3.level4.level5.level6.level7.level8.level9 + .level10, + ).toBe('deep value'); + }); + + it('handles nested arrays within objects', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + users: [ + { + name: 'John', + roles: ['admin', 'user'], + permissions: { + read: true, + write: false, + scopes: ['api', 'web'], + }, + }, + ], + }, + keys: ['users'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data.users[0].roles).toEqual(['admin', 'user']); + expect(data.users[0].permissions.scopes).toEqual(['api', 'web']); + }); + }); + + describe('Special characters in field names', () => { + it('handles dots in field names', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + 'user.name': 'John', + 'config.setting.value': '123', + }, + keys: ['user.name', 'config.setting.value'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data['user.name']).toBe('John'); + expect(data['config.setting.value']).toBe('123'); + }); + + it('handles brackets in field names', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + 'items[0]': 'first', + 'items[1]': 'second', + 'data[key]': 'value', + }, + keys: ['items[0]', 'items[1]', 'data[key]'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data['items[0]']).toBe('first'); + expect(data['items[1]']).toBe('second'); + }); + + it('handles quotes in field names', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + "field'with'single": 'value1', + 'field"with"double': 'value2', + }, + keys: ["field'with'single", 'field"with"double'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data["field'with'single"]).toBe('value1'); + expect(data['field"with"double']).toBe('value2'); + }); + + it('handles backslashes in field names', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + 'path\\to\\file': 'C:\\Users\\test', + 'escape\\n\\t': 'literal', + }, + keys: ['path\\to\\file', 'escape\\n\\t'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data['path\\to\\file']).toBe('C:\\Users\\test'); + }); + + it('handles spaces and hyphens in field names', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + 'first name': 'John', + 'last-name': 'Doe', + ' leading-trailing ': 'spaces', + }, + keys: ['first name', 'last-name', ' leading-trailing '], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data['first name']).toBe('John'); + expect(data['last-name']).toBe('Doe'); + }); + }); + + describe('Special values', () => { + it('handles null values', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + name: 'John', + middleName: null, + age: null, + }, + keys: ['name', 'middleName', 'age'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data.name).toBe('John'); + expect(data.middleName).toBeNull(); + expect(data.age).toBeNull(); + }); + + it('handles boolean values', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + active: true, + verified: false, + settings: { + notifications: true, + darkMode: false, + }, + }, + keys: ['active', 'verified', 'settings'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data.active).toBe(true); + expect(data.verified).toBe(false); + expect(data.settings.notifications).toBe(true); + expect(data.settings.darkMode).toBe(false); + }); + + it('handles numeric edge cases', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + zero: 0, + negative: -42, + float: 3.14159, + largeNumber: 9007199254740991, // MAX_SAFE_INTEGER + scientific: 1.5e10, + }, + keys: ['zero', 'negative', 'float', 'largeNumber', 'scientific'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data.zero).toBe(0); + expect(data.negative).toBe(-42); + expect(data.float).toBeCloseTo(3.14159); + expect(data.largeNumber).toBe(9007199254740991); + }); + }); + + describe('Control characters and special strings', () => { + it('handles newlines and tabs in values', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + multiline: 'Line 1\nLine 2\nLine 3', + tabbed: 'Col1\tCol2\tCol3', + mixed: 'Text\twith\ttabs\nand\nnewlines', + }, + keys: ['multiline', 'tabbed', 'mixed'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data.multiline).toContain('\n'); + expect(data.tabbed).toContain('\t'); + }); + + it('handles carriage returns', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + windowsLines: 'Line 1\r\nLine 2\r\nLine 3', + oldMac: 'Line 1\rLine 2', + }, + keys: ['windowsLines', 'oldMac'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data.windowsLines).toContain('\r\n'); + }); + + it('handles very long strings', async () => { + const longString = 'x'.repeat(10000); + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + longField: longString, + }, + keys: ['longField'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data.longField.length).toBe(10000); + }); + }); + + describe('Array variations', () => { + it('handles arrays of mixed types', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + mixed: [1, 'two', true, null, { nested: 'object' }, [1, 2, 3]], + }, + keys: ['mixed'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data.mixed).toHaveLength(6); + expect(data.mixed[0]).toBe(1); + expect(data.mixed[1]).toBe('two'); + expect(data.mixed[2]).toBe(true); + expect(data.mixed[3]).toBeNull(); + expect(data.mixed[4]).toEqual({ nested: 'object' }); + expect(data.mixed[5]).toEqual([1, 2, 3]); + }); + + it('handles large arrays', async () => { + const largeArray = Array.from({ length: 100 }, (_, i) => ({ + id: i, + value: `item-${i}`, + })); + + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + items: largeArray, + }, + keys: ['items'], + }); + + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data.items).toHaveLength(100); + expect(data.items[99]).toEqual({ id: 99, value: 'item-99' }); + }); + }); +}); diff --git a/tests/routes/submission.test.ts b/tests/routes/submission.test.ts new file mode 100644 index 0000000..c725259 --- /dev/null +++ b/tests/routes/submission.test.ts @@ -0,0 +1,136 @@ +import { beforeEach, describe, expect, it } from 'vitest'; + +import { + createTestForm, + createTestUser, + createUnauthenticatedCaller, + getTestDb, + type TestForm, + type TestUser, +} from '../helpers'; + +describe('Public Submission (formData.setFormData)', () => { + let user: TestUser; + let testForm: TestForm; + let publicCaller: Awaited>; + + beforeEach(async () => { + user = await createTestUser({ + email: 'submission@example.com', + }); + testForm = await createTestForm({ + userId: user.id, + title: 'Public Form', + enableEmailNotifications: false, // Skip email for tests + }); + publicCaller = await createUnauthenticatedCaller(); + }); + + describe('FormData-like JSON submission', () => { + it('accepts and stores form data', async () => { + const result = await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + name: 'John Doe', + email: 'john@example.com', + message: 'Hello World', + }, + keys: ['name', 'email', 'message'], + }); + + expect(result).toBeDefined(); + }); + + it('updates form keys with new field names', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { name: 'Jane' }, + keys: ['name'], + }); + + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { email: 'jane@example.com' }, + keys: ['name', 'email'], + }); + + // Verify the form keys were updated + const db = getTestDb(); + const form = await db.query.forms.findFirst({ + where: (table, { eq }) => eq(table.id, testForm.id), + }); + + const keys = JSON.parse(form?.keys ?? '[]'); + expect(keys).toContain('name'); + expect(keys).toContain('email'); + }); + }); + + describe('JSON submission', () => { + it('stores JSON data correctly', async () => { + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { + nested: { value: 123 }, + array: [1, 2, 3], + boolean: true, + }, + keys: ['nested', 'array', 'boolean'], + }); + + // Verify data was stored + const db = getTestDb(); + const submissions = await db.query.formDatas.findMany({ + where: (table, { eq }) => eq(table.formId, testForm.id), + }); + + expect(submissions).toHaveLength(1); + const data = JSON.parse(submissions[0]?.data ?? '{}'); + expect(data.nested).toEqual({ value: 123 }); + expect(data.array).toEqual([1, 2, 3]); + expect(data.boolean).toBe(true); + }); + }); + + describe('Error handling', () => { + it('handles submission to nonexistent form (transaction will fail)', async () => { + // Note: setFormData doesn't check if form exists before insert, + // but the foreign key constraint will cause the transaction to fail + await expect( + publicCaller.formData.setFormData({ + formId: 'nonexistent12345', + data: { test: 'data' }, + keys: ['test'], + }), + ).rejects.toThrow(); + }); + }); + + describe('Form updates timestamp', () => { + it('updates form updatedAt on submission', async () => { + const db = getTestDb(); + + // Get initial form state + const before = await db.query.forms.findFirst({ + where: (table, { eq }) => eq(table.id, testForm.id), + }); + + // Small delay to ensure timestamp difference + await new Promise((resolve) => setTimeout(resolve, 10)); + + await publicCaller.formData.setFormData({ + formId: testForm.id, + data: { test: 'timestamp' }, + keys: ['test'], + }); + + const after = await db.query.forms.findFirst({ + where: (table, { eq }) => eq(table.id, testForm.id), + }); + + expect(after?.updatedAt).toBeDefined(); + // The updatedAt should be different (or at least defined) + expect(after?.updatedAt).not.toEqual(before?.updatedAt); + }); + }); +}); diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 0000000..f65700d --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,29 @@ +{ + "extends": "../packages/config/tsconfig/base.json", + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "baseUrl": ".", + "paths": { + "@formbase/api": ["../packages/api"], + "@formbase/api/*": ["../packages/api/*"], + "@formbase/db": ["../packages/db"], + "@formbase/db/*": ["../packages/db/*"], + "@formbase/auth": ["../packages/auth"], + "@formbase/auth/*": ["../packages/auth/*"], + "@formbase/env": ["../packages/env"], + "@formbase/utils": ["../packages/utils"], + "@formbase/utils/*": ["../packages/utils/*"], + "~/*": ["../apps/web/src/*"] + }, + "types": ["vitest/globals", "node"] + }, + "include": ["**/*.ts", "**/*.test.ts"], + "exclude": ["node_modules"] +} diff --git a/tests/vitest.config.ts b/tests/vitest.config.ts new file mode 100644 index 0000000..7fd980c --- /dev/null +++ b/tests/vitest.config.ts @@ -0,0 +1,51 @@ +import path from 'path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + setupFiles: ['./vitest.setup.ts'], + include: ['**/*.test.ts'], + exclude: ['**/e2e/**', '**/node_modules/**'], + testTimeout: 10000, + hookTimeout: 10000, + pool: 'forks', + poolOptions: { + forks: { + singleFork: true, // Serial execution for SQLite + }, + }, + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html', 'lcov'], + reportsDirectory: './coverage', + include: [ + '../packages/api/**/*.ts', + '../apps/web/src/app/api/**/*.ts', + ], + exclude: [ + '**/*.test.ts', + '**/*.spec.ts', + '**/node_modules/**', + '**/dist/**', + ], + thresholds: { + lines: 50, + functions: 50, + branches: 50, + statements: 50, + }, + }, + }, + resolve: { + alias: { + '@formbase/api': path.resolve(__dirname, '../packages/api'), + '@formbase/db': path.resolve(__dirname, '../packages/db'), + '@formbase/auth': path.resolve(__dirname, '../packages/auth'), + '@formbase/env': path.resolve(__dirname, '../packages/env'), + '@formbase/utils': path.resolve(__dirname, '../packages/utils'), + '~': path.resolve(__dirname, '../apps/web/src'), + }, + }, +}); diff --git a/tests/vitest.setup.ts b/tests/vitest.setup.ts new file mode 100644 index 0000000..fc0aa99 --- /dev/null +++ b/tests/vitest.setup.ts @@ -0,0 +1,28 @@ +import { afterAll, afterEach, beforeAll } from 'vitest'; + +import { + resetDatabase, + setupTestDatabase, + teardownTestDatabase, +} from './helpers/db'; + +// Set test environment variables before any imports +process.env['SKIP_ENV_VALIDATION'] = 'true'; +process.env['NODE_ENV'] = 'test'; +process.env['DATABASE_URL'] = 'file::memory:?cache=shared'; +process.env['BETTER_AUTH_SECRET'] = + 'test-secret-minimum-32-characters-long-for-testing'; +process.env['NEXT_PUBLIC_APP_URL'] = 'http://localhost:3000'; +process.env['ALLOW_SIGNIN_SIGNUP'] = 'true'; + +beforeAll(async () => { + await setupTestDatabase(); +}); + +afterEach(async () => { + await resetDatabase(); +}); + +afterAll(async () => { + await teardownTestDatabase(); +}); diff --git a/turbo.json b/turbo.json index ebab621..969e455 100644 --- a/turbo.json +++ b/turbo.json @@ -103,6 +103,24 @@ "dependsOn": ["^build", "build"], "cache": false, "persistent": true + }, + "test": { + "dependsOn": ["^build"], + "inputs": ["tests/**/*.ts", "packages/**/*.ts"], + "outputs": ["tests/coverage/**"], + "cache": false + }, + "test:coverage": { + "dependsOn": ["^build"], + "inputs": ["tests/**/*.ts", "packages/**/*.ts"], + "outputs": ["tests/coverage/**"], + "cache": false + }, + "test:e2e": { + "dependsOn": ["build"], + "inputs": ["tests/e2e/**/*.ts"], + "outputs": ["tests/playwright-report/**", "tests/test-results/**"], + "cache": false } } }