From bd5e3098518276f45a00e7c63f22e726cf8d003e Mon Sep 17 00:00:00 2001 From: SIAX Foundation Bot Date: Mon, 20 Jul 2026 00:01:37 +0200 Subject: [PATCH 1/3] =?UTF-8?q?chore(foundation):=20Wave=201=20scaffolding?= =?UTF-8?q?=20=E2=80=94=20CI=20gate=20/=20Dockerfile=20/=20.env.example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 35 ++++++++++++++++++++ .env.example | 61 ++++++++++++++++++++++++++++++++++ .gitea/workflows/ci.yml | 73 ++++++++++++++++++++++++++++++++++++++--- Dockerfile | 71 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 235 insertions(+), 5 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0fc740c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,35 @@ +# Keep the build context small + reproducible. Do NOT exclude app source dirs +# (app/, components/, hooks/, lib/, packages/, src/, styles/, public/) -- next +# build needs them. +node_modules +.next +out +build + +.git +.gitea +.github + +# local env / secrets -- never ship into an image +.env +.env.* +!.env.example + +.vercel +*.tsbuildinfo +next-env.d.ts + +# python services + their large sample dumps are not part of the Next build +**/__pycache__ +*.pyc +services/scrapers/output + +# editor / OS noise +.DS_Store +*.log +npm-debug.log* +.pnpm-debug.log* + +# the Docker files themselves +Dockerfile +.dockerignore diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7653cd8 --- /dev/null +++ b/.env.example @@ -0,0 +1,61 @@ +# ============================================================================= +# 0pt web console -- example environment +# ============================================================================= +# Copy to `.env` (gitignored) and fill in real values. Every key below is +# actually referenced by the code (derived from `process.env.*` usage); nothing +# here is invented. All secrets are placeholders -- do NOT commit real values. +# +# The app is designed to build and boot with these UNSET: lib/db.ts and +# lib/stripe.ts are lazy + env-guarded, and the storage/payment/doc routes +# return HTTP 501 ("not configured") until the relevant keys are provided. + +# ---- Core ------------------------------------------------------------------- +# Public base URL of this app (used for absolute links / callbacks). +NEXT_PUBLIC_APP_URL=http://localhost:3000 + +# ---- Database (Postgres) ---------------------------------------------------- +# First one set wins: DATABASE_URL -> POSTGRES_URL -> POSTGRES_PRISMA_URL. +DATABASE_URL=postgres://user:password@localhost:5432/opt +POSTGRES_URL= +POSTGRES_PRISMA_URL= + +# ---- Queue / cache (Redis) -------------------------------------------------- +REDIS_URL=redis://localhost:6379 + +# ---- Auth ------------------------------------------------------------------- +# Session-signing secret (generate a long random string). +AUTH_SECRET=change-me-long-random-string +# Optional dev-only login shortcut key (leave unset in production). +DEV_LOGIN_KEY= +# Encryption key for stored DB-connection grants (/api/connections/db-grant). +GRANT_ENC_KEY=change-me-32-byte-key +# Shared token for internal/service-to-service calls. +INTERNAL_TOKEN=change-me-internal-token + +# ---- Payments (Stripe) ------------------------------------------------------ +STRIPE_SECRET_KEY=sk_test_xxx +STRIPE_WEBHOOK_SECRET=whsec_xxx + +# ---- Object storage (Cloudflare R2 / S3-compatible) ------------------------- +# All three required to enable /api/uploads; otherwise it returns 501. +R2_ENDPOINT=https://.r2.cloudflarestorage.com +R2_BUCKET=opt-uploads +R2_ACCESS_KEY_ID=change-me +# Optional local blob dir fallback used when R2 is not configured. +OPT_LOCAL_BLOB_DIR=./.opt-blobs + +# ---- Document intelligence (Azure) ------------------------------------------ +# Powers document extraction routes; unset => extraction "not configured". +DOC_INTELLIGENCE_ENDPOINT=https://.cognitiveservices.azure.com +DOC_INTELLIGENCE_KEY=change-me +DOC_INTELLIGENCE_API_VERSION=2024-11-30 + +# ---- Engine / integrations -------------------------------------------------- +# URL of the 0pt data engine (services/api orchestrator). +ENGINE_URL=http://localhost:8000 +# Odoo base URL for the bills/write-back adapter. +ODOO_BASE_URL=https:// + +# ---- Test-only (tests/p0 real-infra suite; not needed to run the app) -------- +# ST0RE_URL= +# ST0RE_TEST_TENANT_ID= diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 61d8982..ba24a43 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,10 +1,73 @@ -name: Scraper CI -on: [push] +name: CI (SIAX Cloud) + +# Self-hosted CI on SIAX Cloud (Gitea Actions, runners siax-runner-1/2), +# following the fleet-wide pattern (quality gate -> advisory security scan). +# +# WHAT THIS REPO ACTUALLY SHIPS (0pt web console): a Next.js 16 app. Its +# package.json exposes exactly four scripts -- build / dev / lint / start -- +# so the BLOCKING quality gate below runs the one real, installed, functional +# build script (`pnpm run build` == `next build`), which type-checks and +# compiles the whole app. +# +# Deliberately NOT run here (honest gaps, additive Wave-1 scope only): +# * lint -> the "lint" script is `eslint .` but eslint is NOT in the +# dependency tree (zero eslint entries in pnpm-lock.yaml), so a +# blocking lint step would fail on "eslint: not found" for a +# missing-devDependency reason, not a code reason. Adding eslint +# is a source/package.json change (Wave 2/3), not Wave-1 +# scaffolding. Re-enable once eslint is a real devDependency. +# * typecheck-> no `typecheck` script exists, and next.config.mjs sets +# `typescript.ignoreBuildErrors: true` on purpose; `next build` +# already runs the TS compiler. +# * test -> tests/p0/*.test.ts exist but there is NO test runner installed +# (no vitest/jest in pnpm-lock.yaml) and no `test` script, so +# there is nothing to invoke. Wiring a runner is Wave 2/3. +# +# The security job is intentionally advisory (separate job, `--exit-code 0`) so +# it never blocks merges, matching the fleet convention. +on: + push: + branches: ["**"] + pull_request: + workflow_dispatch: + schedule: + - cron: "0 4 * * *" # daily safety re-run so CI runs even without a push + jobs: - lint: + quality: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Check Python files + - uses: actions/setup-node@v4 + with: + node-version: "22" + - name: Enable corepack + pin pnpm@9 (pnpm-lock.yaml is lockfileVersion 9.0) run: | - echo "Scraper code verified" + corepack enable + corepack prepare pnpm@9 --activate + # --ignore-workspace: the repo's pnpm-workspace.yaml is a v0-export artifact + # that carries only an `allowBuilds:` block and NO `packages:` field, which + # makes pnpm abort with "packages field missing or empty". There are no real + # workspace members (only the root package.json), so ignoring the workspace + # file is a semantic no-op and lets the frozen install resolve cleanly. + - name: Install (frozen lockfile) + run: pnpm install --frozen-lockfile --ignore-workspace + - name: Build (next build -- blocking quality gate, no `|| true`) + # --ignore-workspace: `pnpm run` also re-parses pnpm-workspace.yaml and aborts + # on the missing `packages:` field unless the workspace is ignored (see above). + run: pnpm --ignore-workspace run build + + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Install Trivy natively and scan the checked-out workspace directly. + # (The `docker run -v "$PWD:/repo" aquasec/trivy` pattern is a silent no-op + # on this Docker-outside-of-Docker runner -- $PWD inside the job container + # is not a real path on the Docker daemon host, so it scans nothing.) + - name: Install Trivy + run: | + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + - name: Trivy scan (vuln + secrets, advisory) + run: | + trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 0 . diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2746587 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,71 @@ +# ============================================================================= +# 0pt web console -- Dockerfile for Coolify (SIAX Cloud) +# ============================================================================= +# This repo's deployable service is the Next.js 16 app at the repo root +# (package.json `start` == `next start`, liveness at GET /api/health). Unlike +# the tsx backend services elsewhere in the fleet (aud0/n0d/api0), a Next app +# needs a real build step, so this is a multi-stage build: install+build, then +# a slim runtime that runs the built output as the non-root `node` user. +# +# node:24-alpine matches the tag already proven in ../aud0 and ../api0. `wget` +# is added to the runtime stage so Coolify's container healthcheck has a binary +# to hit /api/health (bare node:*-alpine ships neither curl nor wget). +# +# next.config.mjs does NOT set `output: "standalone"` (that would be a source +# change, out of Wave-1 scope), so the runtime stage carries node_modules + the +# .next build output and runs `next start` directly. +# ============================================================================= + +# ---- build stage ------------------------------------------------------------ +FROM node:24-alpine AS builder +WORKDIR /app + +ENV NEXT_TELEMETRY_DISABLED=1 + +# Enable corepack (pnpm@9 pinned below to match pnpm-lock.yaml lockfileVersion 9.0). +RUN corepack enable + +# Install deps against the frozen lockfile first for layer caching. +# --ignore-workspace: the repo's pnpm-workspace.yaml is a v0-export artifact that +# carries only an `allowBuilds:` block and NO `packages:` field, which makes pnpm +# abort with "packages field missing or empty". There are no real workspace +# members (only the root package.json), so ignoring the workspace file is +# semantically a no-op and lets the frozen install resolve the root deps cleanly. +COPY package.json pnpm-lock.yaml .npmrc ./ +RUN corepack prepare pnpm@9 --activate && pnpm install --frozen-lockfile --ignore-workspace + +# Build the app. The lib/* clients (db, stripe, ...) are lazy + env-guarded, so +# `next build` succeeds without any runtime secrets present. +# --ignore-workspace again here: `COPY . .` reintroduces the v0 pnpm-workspace.yaml, +# and `pnpm run` re-parses it (same "packages field missing" abort) unless ignored. +COPY . . +RUN pnpm --ignore-workspace run build + +# ---- runtime stage ---------------------------------------------------------- +FROM node:24-alpine AS runtime +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV HOSTNAME=0.0.0.0 +ENV PORT=3000 + +RUN apk add --no-cache wget + +# Built artifacts + resolved dependencies from the build stage. .next is chowned +# so `next start` can write its runtime cache as the non-root user. +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder --chown=node:node /app/.next ./.next +COPY --from=builder /app/public ./public +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/next.config.mjs ./next.config.mjs + +USER node + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD wget -qO- http://127.0.0.1:3000/api/health >/dev/null 2>&1 || exit 1 + +# Invoke the next binary directly (pnpm/corepack are not set up in this stage). +CMD ["node_modules/.bin/next", "start"] -- 2.54.0 From 844220d9f59ce8526238a1db6d271b72ce0b19d8 Mon Sep 17 00:00:00 2001 From: SIAX Foundation Bot Date: Sat, 25 Jul 2026 13:54:36 +0200 Subject: [PATCH 2/3] feat(foundation): Wave 3 integrity/persistence/build fixes --- .gitignore | 7 +- .npmrc | 2 + eslint.config.mjs | 29 + package.json | 19 +- .../schemas/0pt.iceberg_table.schema.json | 36 + .../schemas/0pt.lineage_event.schema.json | 53 + packages/schemas/0pt.signal.schema.json | 31 + pnpm-lock.yaml | 1828 ++++++++++++++++- pnpm-workspace.yaml | 20 +- src/clients/aud0-client.ts | 98 + src/clients/cl0ud-client.ts | 79 + src/clients/n0d-client.ts | 84 + src/clients/st0re-client.ts | 93 + src/services/dataset-service.ts | 112 + src/services/source-service.ts | 85 + src/types/source.ts | 45 + tsconfig.check.json | 10 + vitest.config.ts | 23 + 18 files changed, 2616 insertions(+), 38 deletions(-) create mode 100644 eslint.config.mjs create mode 100644 packages/schemas/0pt.iceberg_table.schema.json create mode 100644 packages/schemas/0pt.lineage_event.schema.json create mode 100644 packages/schemas/0pt.signal.schema.json create mode 100644 src/clients/aud0-client.ts create mode 100644 src/clients/cl0ud-client.ts create mode 100644 src/clients/n0d-client.ts create mode 100644 src/clients/st0re-client.ts create mode 100644 src/services/dataset-service.ts create mode 100644 src/services/source-service.ts create mode 100644 src/types/source.ts create mode 100644 tsconfig.check.json create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index f650315..963d8ee 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,9 @@ yarn-error.log* # typescript *.tsbuildinfo -next-env.d.ts \ No newline at end of file +next-env.d.ts +# pnpm local store (created when store-dir defaults into the project) +.pnpm-store/ + +# interim LocalBlobStore output (src/adapters/local-blob-store.ts) +.data/ diff --git a/.npmrc b/.npmrc index d099685..21d37e3 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,3 @@ verify-deps-before-run=false +include-workspace-root=true +confirmModulesPurge=false diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..d77ba4c --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,29 @@ +// Flat ESLint config (ESLint 9) for 0pt's capability code. +// +// Scope: src/ + tests/ — the backend capability layer this repo's Wave 2/3 +// hardening actually owns. The v0-generated Next.js UI (app/, components/, +// hooks/, lib/) is intentionally NOT linted here; wiring the full Next lint +// preset over that generated surface is a separate concern. `pnpm run lint` +// runs this over src + tests. +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: ["node_modules/**", ".next/**", "**/*.d.ts"], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["src/**/*.ts", "tests/**/*.ts"], + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + }, + rules: { + // Underscore-prefixed args are deliberate no-op/interface-shaped params + // (e.g. the local/no-op client method params in src/clients/*). + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], + }, + }, +); diff --git a/package.json b/package.json index 832710f..0f5de3f 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,15 @@ "name": "my-v0-project", "version": "0.1.0", "private": true, + "packageManager": "pnpm@9.15.9", "scripts": { "build": "next build", "dev": "next dev", - "lint": "eslint .", - "start": "next start" + "lint": "eslint src tests", + "start": "next start", + "typecheck": "tsc -p tsconfig.check.json", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@hookform/resolvers": "^3.10.0", @@ -63,18 +67,23 @@ "zod": "3.25.76" }, "devDependencies": { + "@eslint/js": "^9.15.0", "@tailwindcss/postcss": "^4.1.9", "@types/node": "^22", "@types/react": "^19", "@types/react-dom": "^19", + "eslint": "^9.15.0", "postcss": "^8.5", "tailwindcss": "^4.1.9", "tw-animate-css": "1.3.3", - "typescript": "^5" + "typescript": "^5", + "typescript-eslint": "^8.15.0", + "vitest": "^3.2.0" }, "pnpm": { "onlyBuiltDependencies": [ - "sharp" + "sharp", + "esbuild" ] } -} \ No newline at end of file +} diff --git a/packages/schemas/0pt.iceberg_table.schema.json b/packages/schemas/0pt.iceberg_table.schema.json new file mode 100644 index 0000000..bbf2dcf --- /dev/null +++ b/packages/schemas/0pt.iceberg_table.schema.json @@ -0,0 +1,36 @@ +{ + "$id": "0pt.iceberg_table.schema.v1", + "type": "object", + "required": [ + "table_id", + "tenant_id", + "project_id", + "table_name", + "namespace", + "storage_uri", + "table_format", + "status", + "created_at" + ], + "properties": { + "table_id": { "type": "string", "description": "Reference into ST0RE's table registry (IcebergTable.tableId, st0re_iceberg_tables)." }, + "tenant_id": { "type": "string" }, + "project_id": { "type": "string" }, + "table_name": { "type": "string" }, + "namespace": { "type": "string", "description": "ST0RE lakehouse namespace name." }, + "storage_uri": { "type": "string", "description": "Object-store URI for the table's data/metadata, e.g. s3://siax-tenants/.../table." }, + "catalog_ref": { "type": "string", "description": "Opaque reference into the external Iceberg catalog — ST0RE does not resolve it." }, + "table_format": { "type": "string", "enum": ["iceberg", "delta", "hudi"] }, + "current_snapshot_id": { "type": "string" }, + "schema_version": { "type": "string" }, + "partition_spec": { "type": "array", "items": { "type": "string" } }, + "retention_policy": { "type": "string" }, + "data_residency": { "type": "string" }, + "pii_level": { "type": "string", "enum": ["none", "low", "medium", "high"] }, + "owner_product": { "type": "string" }, + "status": { "type": "string", "enum": ["active", "deprecated", "archived"] }, + "correlation_id": { "type": "string" }, + "created_at": { "type": "string" }, + "created_by": { "type": "string" } + } +} diff --git a/packages/schemas/0pt.lineage_event.schema.json b/packages/schemas/0pt.lineage_event.schema.json new file mode 100644 index 0000000..6a139d0 --- /dev/null +++ b/packages/schemas/0pt.lineage_event.schema.json @@ -0,0 +1,53 @@ +{ + "$id": "0pt.lineage_event.schema.v1", + "type": "object", + "required": [ + "lineage_event_id", + "tenant_id", + "project_id", + "producer", + "input_assets", + "output_assets", + "created_at" + ], + "properties": { + "lineage_event_id": { "type": "string" }, + "tenant_id": { "type": "string" }, + "project_id": { "type": "string" }, + "producer": { + "type": "object", + "description": "The capability/app that produced this lineage edge.", + "required": ["app_id", "capability_id"], + "properties": { + "app_id": { "type": "string" }, + "capability_id": { "type": "string" } + } + }, + "input_assets": { + "type": "array", + "items": { + "type": "object", + "required": ["asset_type", "asset_id"], + "properties": { + "asset_type": { "type": "string", "enum": ["dataset", "iceberg_table", "artifact", "source", "document", "signal"] }, + "asset_id": { "type": "string" } + } + } + }, + "output_assets": { + "type": "array", + "items": { + "type": "object", + "required": ["asset_type", "asset_id"], + "properties": { + "asset_type": { "type": "string", "enum": ["dataset", "iceberg_table", "artifact", "source", "document", "signal"] }, + "asset_id": { "type": "string" } + } + } + }, + "st0re_artifacts": { "type": "array", "items": { "type": "string" } }, + "quality_report_id": { "type": "string" }, + "correlation_id": { "type": "string" }, + "created_at": { "type": "string" } + } +} diff --git a/packages/schemas/0pt.signal.schema.json b/packages/schemas/0pt.signal.schema.json new file mode 100644 index 0000000..e4e5ef9 --- /dev/null +++ b/packages/schemas/0pt.signal.schema.json @@ -0,0 +1,31 @@ +{ + "$id": "0pt.signal.schema.v1", + "type": "object", + "required": [ + "signal_id", + "tenant_id", + "project_id", + "signal_type", + "signal_strength", + "entity_type", + "entity_id", + "observed_at", + "created_at" + ], + "properties": { + "signal_id": { "type": "string" }, + "tenant_id": { "type": "string" }, + "project_id": { "type": "string" }, + "signal_type": { "type": "string", "description": "e.g. hiring_growth, funding_round, tech_adoption, leadership_change." }, + "signal_strength": { "type": "number", "description": "Normalized 0..1 confidence/intensity of the signal.", "minimum": 0, "maximum": 1 }, + "entity_type": { "type": "string", "enum": ["company", "person", "domain", "vehicle"] }, + "entity_id": { "type": "string" }, + "value": { "description": "Signal payload value (type depends on signal_type)." }, + "unit": { "type": "string" }, + "source_id": { "type": "string", "description": "References 0pt.source.schema.v1's source_id." }, + "confidence_level": { "type": "string", "enum": ["low", "medium", "high"] }, + "observed_at": { "type": "string" }, + "correlation_id": { "type": "string" }, + "created_at": { "type": "string" } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6201031..511a8e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -165,6 +165,9 @@ importers: specifier: 3.25.76 version: 3.25.76 devDependencies: + '@eslint/js': + specifier: ^9.15.0 + version: 9.39.5 '@tailwindcss/postcss': specifier: ^4.1.9 version: 4.3.1 @@ -177,6 +180,9 @@ importers: '@types/react-dom': specifier: ^19 version: 19.2.3(@types/react@19.2.17) + eslint: + specifier: ^9.15.0 + version: 9.39.5(jiti@2.7.0) postcss: specifier: ^8.5 version: 8.5.15 @@ -189,6 +195,12 @@ importers: typescript: specifier: ^5 version: 5.9.3 + typescript-eslint: + specifier: ^8.15.0 + version: 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + vitest: + specifier: ^3.2.0 + version: 3.2.7(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0) packages: @@ -206,6 +218,200 @@ packages: '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.6': + resolution: {integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.5': + resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -226,6 +432,26 @@ packages: peerDependencies: react-hook-form: ^7.0.0 + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -256,105 +482,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -415,28 +625,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@16.0.10': resolution: {integrity: sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@16.0.10': resolution: {integrity: sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@16.0.10': resolution: {integrity: sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@16.0.10': resolution: {integrity: sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==} @@ -1106,6 +1312,131 @@ packages: '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -1147,28 +1478,24 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.1': resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.1': resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.1': resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.1': resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==} @@ -1201,6 +1528,9 @@ packages: '@tailwindcss/postcss@4.3.1': resolution: {integrity: sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -1228,6 +1558,15 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@22.20.0': resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==} @@ -1239,6 +1578,65 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@typescript-eslint/eslint-plugin@8.65.0': + resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.65.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.65.0': + resolution: {integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.65.0': + resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.65.0': + resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.65.0': + resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.65.0': + resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.65.0': + resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.65.0': + resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.65.0': + resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.65.0': + resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vercel/analytics@1.3.1': resolution: {integrity: sha512-xhSlYgAuJ6Q4WQGkzYTLmXwhYl39sWjoMA3nHxfkvG+WdBT25c563a7QhwwKivEOZtPJXifYHR1m2ihoisbWyA==} peerDependencies: @@ -1250,10 +1648,63 @@ packages: react: optional: true + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.6: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + autoprefixer@10.5.0: resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} engines: {node: ^10 || ^12 || >=14} @@ -1261,19 +1712,53 @@ packages: peerDependencies: postcss: ^8.1.0 + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + baseline-browser-mapping@2.10.38: resolution: {integrity: sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==} engines: {node: '>=6.0.0'} hasBin: true + brace-expansion@1.1.16: + resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} + + brace-expansion@5.0.8: + resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + engines: {node: 20 || >=22} + browserslist@4.28.4: resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + caniuse-lite@1.0.30001799: resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -1290,6 +1775,20 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1343,9 +1842,25 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1376,27 +1891,158 @@ packages: resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} engines: {node: '>=10.13.0'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.5: + resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@5.4.0: resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} engines: {node: '>=6.0.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.3: + resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} + fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + input-otp@1.4.1: resolution: {integrity: sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==} peerDependencies: @@ -1407,6 +2053,17 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -1414,6 +2071,29 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -1449,28 +2129,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -1488,6 +2164,13 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} @@ -1495,6 +2178,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lucide-react@0.454.0: resolution: {integrity: sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==} peerDependencies: @@ -1503,11 +2189,24 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.15: resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -1543,9 +2242,44 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -1561,9 +2295,17 @@ packages: resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==} engines: {node: '>=12'} + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + react-day-picker@9.8.0: resolution: {integrity: sha512-E0yhhg7R+pdgbl/2toTb0xBhsEAtmAx1l7qjIWYfcxOy8w4rTSVfbtBoSzVVhPwKP/5E9iL38LivzoE3AQDhCQ==} engines: {node: '>=18'} @@ -1650,6 +2392,15 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1665,6 +2416,17 @@ packages: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + sonner@1.7.4: resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==} peerDependencies: @@ -1675,6 +2437,19 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + stripe@22.2.3: resolution: {integrity: sha512-9lrggvLtjO4Ef5WXH4t50ckHJcJgTD72xFB+KJgZCSszAH9zMV1BqU6PwmWbRLdcdX7LK6PbErBapiGK7pIb+g==} engines: {node: '>=18'} @@ -1697,6 +2472,10 @@ packages: babel-plugin-macros: optional: true + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + tailwind-merge@3.6.0: resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} @@ -1715,12 +2494,51 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} tw-animate-css@1.3.3: resolution: {integrity: sha512-tXE2TRWrskc4TU3RDd7T8n8Np/wCfoeH9gz22c7PzYqNPQ9FBGFbWWzwL0JyHcFp+jHozmF76tbHfPAx22ua2Q==} + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.65.0: + resolution: {integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1735,6 +2553,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -1769,6 +2590,97 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -1785,6 +2697,130 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0))': + dependencies: + eslint: 9.39.5(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.6': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.5': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -1806,6 +2842,22 @@ snapshots: dependencies: react-hook-form: 7.80.0(react@19.2.0) + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@img/colour@1.1.0': optional: true @@ -2640,6 +3692,81 @@ snapshots: '@radix-ui/rect@1.1.0': {} + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -2713,6 +3840,11 @@ snapshots: postcss: 8.5.15 tailwindcss: 4.3.1 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/d3-array@3.2.2': {} '@types/d3-color@3.1.3': {} @@ -2737,6 +3869,12 @@ snapshots: '@types/d3-timer@3.0.2': {} + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + '@types/node@22.20.0': dependencies: undici-types: 6.21.0 @@ -2749,6 +3887,97 @@ snapshots: dependencies: csstype: 3.2.3 + '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/type-utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.65.0 + eslint: 9.39.5(jiti@2.7.0) + ignore: 7.0.6 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.65.0 + debug: 4.4.3 + eslint: 9.39.5(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.65.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3) + '@typescript-eslint/types': 8.65.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.65.0': + dependencies: + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 + + '@typescript-eslint/tsconfig-utils@8.65.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.5(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.65.0': {} + + '@typescript-eslint/typescript-estree@8.65.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.65.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3) + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.65.0': + dependencies: + '@typescript-eslint/types': 8.65.0 + eslint-visitor-keys: 5.0.1 + '@vercel/analytics@1.3.1(next@16.0.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)': dependencies: server-only: 0.0.1 @@ -2756,10 +3985,73 @@ snapshots: next: 16.0.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + aria-hidden@1.2.6: dependencies: tslib: 2.8.1 + assertion-error@2.0.1: {} + autoprefixer@10.5.0(postcss@8.5.15): dependencies: browserslist: 4.28.4 @@ -2769,8 +4061,21 @@ snapshots: postcss: 8.5.15 postcss-value-parser: 4.2.0 + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + baseline-browser-mapping@2.10.38: {} + brace-expansion@1.1.16: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.8: + dependencies: + balanced-match: 4.0.4 + browserslist@4.28.4: dependencies: baseline-browser-mapping: 2.10.38 @@ -2779,8 +4084,27 @@ snapshots: node-releases: 2.0.49 update-browserslist-db: 1.2.3(browserslist@4.28.4) + cac@6.7.14: {} + + callsites@3.1.0: {} + caniuse-lite@1.0.30001799: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@2.1.3: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -2801,6 +4125,20 @@ snapshots: - '@types/react' - '@types/react-dom' + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + csstype@3.2.3: {} d3-array@3.2.4: @@ -2845,8 +4183,16 @@ snapshots: date-fns@4.1.0: {} + debug@4.4.3: + dependencies: + ms: 2.1.3 + decimal.js-light@2.5.1: {} + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -2875,18 +4221,175 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 + es-module-lexer@1.7.0: {} + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.5(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.6 + '@eslint/js': 9.39.5 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + eventemitter3@4.0.7: {} + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + fast-equals@5.4.0: {} + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.3 + keyv: 4.5.4 + + flatted@3.4.3: {} + fraction.js@5.3.4: {} + fsevents@2.3.3: + optional: true + get-nonce@1.0.1: {} + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + graceful-fs@4.2.11: {} + has-flag@4.0.0: {} + + ignore@5.3.2: {} + + ignore@7.0.6: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + input-otp@1.4.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: react: 19.2.0 @@ -2894,10 +4397,39 @@ snapshots: internmap@2.0.3: {} + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + jiti@2.7.0: {} js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + lightningcss-android-arm64@1.32.0: optional: true @@ -2947,12 +4479,20 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + lodash@4.18.1: {} loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 + loupe@3.2.1: {} + lucide-react@0.454.0(react@19.2.0): dependencies: react: 19.2.0 @@ -2961,8 +4501,20 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.8 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.16 + + ms@2.1.3: {} + nanoid@3.3.15: {} + natural-compare@1.4.0: {} + next-themes@0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: react: 19.2.0 @@ -2995,8 +4547,39 @@ snapshots: object-assign@4.1.1: {} + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + picocolors@1.1.1: {} + picomatch@4.0.5: {} + postcss-value-parser@4.2.0: {} postcss@8.4.31: @@ -3013,12 +4596,16 @@ snapshots: postgres@3.4.9: {} + prelude-ls@1.2.1: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 + punycode@2.3.1: {} + react-day-picker@9.8.0(react@19.2.0): dependencies: '@date-fns/tz': 1.2.0 @@ -3107,10 +4694,42 @@ snapshots: tiny-invariant: 1.3.3 victory-vendor: 36.9.2 + resolve-from@4.0.0: {} + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + scheduler@0.27.0: {} - semver@7.8.5: - optional: true + semver@7.8.5: {} server-only@0.0.1: {} @@ -3146,6 +4765,14 @@ snapshots: '@img/sharp-win32-x64': 0.34.5 optional: true + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + sonner@1.7.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: react: 19.2.0 @@ -3153,6 +4780,16 @@ snapshots: source-map-js@1.2.1: {} + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + stripe@22.2.3(@types/node@22.20.0): optionalDependencies: '@types/node': 22.20.0 @@ -3162,6 +4799,10 @@ snapshots: client-only: 0.0.1 react: 19.2.0 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + tailwind-merge@3.6.0: {} tailwindcss-animate@1.0.7(tailwindcss@4.3.1): @@ -3174,10 +4815,44 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + tslib@2.8.1: {} tw-animate-css@1.3.3: {} + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} undici-types@6.21.0: {} @@ -3188,6 +4863,10 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.0): dependencies: react: 19.2.0 @@ -3233,4 +4912,93 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + vite-node@3.2.4(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.6(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0): + dependencies: + esbuild: 0.28.1 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.15 + rollup: 4.62.2 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.20.0 + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + + vitest@3.2.7(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0) + vite-node: 3.2.4(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + yocto-queue@0.1.0: {} + zod@3.25.76: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ee81e4e..3c28c45 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,18 @@ -allowBuilds: - sharp: set this to true or false +# SIAX Cloud 0pt workspace. +# +# The deployable Next.js app AND the 0pt capability code (src/, tests/) live at +# the repo ROOT — there are no separate member packages yet (packages/ holds +# JSON schemas only, no package.json). Previously this file carried only a +# placeholder `allowBuilds:` block and NO `packages:` field, which made pnpm +# abort with "packages field missing or empty" on any non-`--ignore-workspace` +# invocation. A present (non-empty) `packages:` field fixes that; the root +# package is included in `pnpm -r` runs via `include-workspace-root=true` +# (.npmrc), which is what lets `pnpm -r run typecheck` / `pnpm -r run test` +# execute the root package's scripts. +packages: + - "packages/*" + +# Build scripts that are allowed to run during install (pnpm >= 9 approval gate). +onlyBuiltDependencies: + - sharp + - esbuild diff --git a/src/clients/aud0-client.ts b/src/clients/aud0-client.ts new file mode 100644 index 0000000..a009f7d --- /dev/null +++ b/src/clients/aud0-client.ts @@ -0,0 +1,98 @@ +/** + * src/clients/aud0-client.ts + * =========================== + * 0pt's thin client for emitting append-only events + lineage edges to AUD0 + * (the SIAX Cloud system of record). Two methods: + * - ingestEvent -> POST {baseUrl}/v1/aud0/events + * - ingestLineageEvent -> POST {baseUrl}/v1/aud0/lineage/events + * + * Convention (identical to cl0ud-client.ts / n0d-client.ts / st0re-client.ts + * / st0re-lakehouse-client.ts): when no base URL is configured the client is + * a NO-OP — it does not attempt any network call. AUD0 is a best-effort, + * out-of-band audit sink for 0pt; a missing AUD0_URL (unit tests, local dev) + * must not turn every capability call into a failed fetch to `undefined`. + * When a base URL IS configured, events are POSTed as-is (the event object is + * the wire body) with `Content-Type: application/json`. + */ + +export interface Aud0Event { + event_id: string; + event_type: string; + tenant_id: string; + project_id: string; + actor_id: string; + app_id: string; + capability_id: string; + resource_type: string; + resource_id: string; + /** Risk level L0–L5 per the SIAX Cloud risk taxonomy. */ + risk_level: string; + payload: Record; + created_at: string; + correlation_id?: string; +} + +export interface Aud0LineageEvent { + lineage_event_id: string; + tenant_id: string; + project_id: string; + app_id: string; + capability_id: string; + source_type: string; + source_id: string; + target_type: string; + target_id: string; + /** e.g. "produced" | "derived_from" | "stored_as". */ + lineage_type: string; + metadata: Record; + created_at: string; + correlation_id?: string; +} + +export interface Aud0Client { + ingestEvent(event: Aud0Event): Promise; + ingestLineageEvent(event: Aud0LineageEvent): Promise; +} + +export function createAud0Client(baseUrl?: string): Aud0Client { + if (!baseUrl) return new NoopAud0Client(); + return new HttpAud0Client(baseUrl); +} + +/** No AUD0_URL configured: audit emission is a no-op (see module doc comment). */ +class NoopAud0Client implements Aud0Client { + async ingestEvent(_event: Aud0Event): Promise { + /* intentional no-op */ + } + async ingestLineageEvent(_event: Aud0LineageEvent): Promise { + /* intentional no-op */ + } +} + +class HttpAud0Client implements Aud0Client { + private readonly baseUrl: string; + + constructor(baseUrl: string) { + // Normalize so `${baseUrl}/v1/...` never produces a double slash. + this.baseUrl = baseUrl.replace(/\/+$/, ""); + } + + async ingestEvent(event: Aud0Event): Promise { + await this.post("/v1/aud0/events", event); + } + + async ingestLineageEvent(event: Aud0LineageEvent): Promise { + await this.post("/v1/aud0/lineage/events", event); + } + + private async post(path: string, body: unknown): Promise { + const res = await fetch(`${this.baseUrl}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!res.ok) { + throw new Error(`AUD0 ${path} error: ${res.status} ${await res.text().catch(() => "")}`); + } + } +} diff --git a/src/clients/cl0ud-client.ts b/src/clients/cl0ud-client.ts new file mode 100644 index 0000000..0bbff6f --- /dev/null +++ b/src/clients/cl0ud-client.ts @@ -0,0 +1,79 @@ +/** + * src/clients/cl0ud-client.ts + * ============================ + * 0pt's client for the CL0UD control-plane policy decision point: + * evaluatePolicy -> POST {baseUrl}/v1/cl0ud/policies/evaluate + * + * Every side-effecting 0pt capability re-consults CL0UD before acting (per + * docs/PRODUCT_PLAN.md §4.1), so this is the single choke point for that. + * + * Convention (identical to aud0-client.ts / n0d-client.ts / st0re-client.ts): + * when no base URL is configured the client runs in LOCAL PASS-THROUGH mode — + * it returns a deterministic `allow` decision WITHOUT any network call, so + * unit tests and local dev are hermetic. This is a dev-mode default only; any + * real deployment MUST set CL0UD_URL so decisions come from the real policy + * engine. The pass-through never invents a `deny`/`approval_required` — it + * only ever allows, and it stamps a clearly local-looking decision id. + */ +import crypto from "crypto"; + +export type PolicyDecision = "allow" | "deny" | "approval_required"; + +export interface EvaluatePolicyInput { + tenant_id: string; + project_id: string; + actor_id: string; + capability_id: string; + resource_type: string; + resource_id: string; + /** Risk level L0–L5 per the SIAX Cloud risk taxonomy. */ + risk_level: string; + correlation_id?: string; +} + +export interface PolicyResult { + decision: PolicyDecision; + decision_id: string; + reason?: string; +} + +export interface Cl0udClient { + evaluatePolicy(input: EvaluatePolicyInput): Promise; +} + +export function createCl0udClient(baseUrl?: string): Cl0udClient { + if (!baseUrl) return new LocalAllowCl0udClient(); + return new HttpCl0udClient(baseUrl); +} + +/** No CL0UD_URL configured: allow-only local pass-through (see module doc comment). */ +class LocalAllowCl0udClient implements Cl0udClient { + async evaluatePolicy(_input: EvaluatePolicyInput): Promise { + return { decision: "allow", decision_id: `pdec_local_${crypto.randomUUID()}` }; + } +} + +class HttpCl0udClient implements Cl0udClient { + private readonly baseUrl: string; + + constructor(baseUrl: string) { + this.baseUrl = baseUrl.replace(/\/+$/, ""); + } + + async evaluatePolicy(input: EvaluatePolicyInput): Promise { + const res = await fetch(`${this.baseUrl}/v1/cl0ud/policies/evaluate`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(input), + }); + if (!res.ok) { + throw new Error(`CL0UD policy evaluate error: ${res.status} ${await res.text().catch(() => "")}`); + } + const body = (await res.json()) as Partial; + return { + decision: (body.decision ?? "deny") as PolicyDecision, + decision_id: body.decision_id ?? `pdec_${crypto.randomUUID()}`, + reason: body.reason, + }; + } +} diff --git a/src/clients/n0d-client.ts b/src/clients/n0d-client.ts new file mode 100644 index 0000000..d0908df --- /dev/null +++ b/src/clients/n0d-client.ts @@ -0,0 +1,84 @@ +/** + * src/clients/n0d-client.ts + * ========================== + * 0pt's client for REQUESTING heavy compute jobs from N0D (the Compute OS). + * 0pt never executes heavy work in-process — it submits an N0D job and holds + * only the returned `job_id` (docs/11-lakehouse-write-plan.md item 7, + * docs/P0_QUERY_SOURCE_FLOW.md): + * createJob -> POST {baseUrl}/v1/n0d/jobs + * + * Convention (identical to aud0-client.ts / cl0ud-client.ts / st0re-client.ts + * / st0re-lakehouse-client.ts): when no base URL is configured the client + * returns a deterministic in-memory MOCK job (id prefixed "mock_job_") with + * NO network call, so unit tests stay hermetic. A real deployment sets + * N0D_URL to submit against the live scheduler. + */ +import crypto from "crypto"; + +export interface CreateJobInput { + tenant_id: string; + project_id: string; + job_type: string; + capability_id: string; + requested_by_app_id: string; + requested_by_actor_id: string; + input_artifact_ids?: string[]; + input_dataset_ids?: string[]; + output_artifact_type: string; + requires_backbone: boolean; + requires_gpu: boolean; + required_node_roles: string[]; + resource_profile: string; + /** Risk level L0–L5 per the SIAX Cloud risk taxonomy. */ + risk_level: string; + correlation_id?: string; +} + +export type N0dJobState = "queued" | "running" | "succeeded" | "failed" | "cancelled"; + +export interface N0dJob extends CreateJobInput { + job_id: string; + state: N0dJobState; + created_at: string; +} + +export interface N0dClient { + createJob(input: CreateJobInput): Promise; +} + +export function createN0dClient(baseUrl?: string): N0dClient { + if (!baseUrl) return new MockN0dClient(); + return new HttpN0dClient(baseUrl); +} + +/** No N0D_URL configured: returns a deterministic mock job (see module doc comment). */ +class MockN0dClient implements N0dClient { + async createJob(input: CreateJobInput): Promise { + return { + ...input, + job_id: `mock_job_${crypto.randomUUID()}`, + state: "queued", + created_at: new Date().toISOString(), + }; + } +} + +class HttpN0dClient implements N0dClient { + private readonly baseUrl: string; + + constructor(baseUrl: string) { + this.baseUrl = baseUrl.replace(/\/+$/, ""); + } + + async createJob(input: CreateJobInput): Promise { + const res = await fetch(`${this.baseUrl}/v1/n0d/jobs`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(input), + }); + if (!res.ok) { + throw new Error(`N0D create-job error: ${res.status} ${await res.text().catch(() => "")}`); + } + return (await res.json()) as N0dJob; + } +} diff --git a/src/clients/st0re-client.ts b/src/clients/st0re-client.ts new file mode 100644 index 0000000..a3aba56 --- /dev/null +++ b/src/clients/st0re-client.ts @@ -0,0 +1,93 @@ +/** + * src/clients/st0re-client.ts + * ============================ + * 0pt's client for the generic ST0RE Artifact registry (snake_case wire + * shape), distinct from `./st0re-lakehouse-client.ts` (the camelCase + * /v1/st0re/lakehouse/* table registry). This one registers an artifact's + * METADATA — a pointer to where the bytes live (provider/bucket/key/region) + * plus content-type/size/checksum/retention/PII/residency: + * createArtifact -> POST {baseUrl}/v1/st0re/artifacts + * + * KNOWN GAP (documented in src/adapters/local-blob-store.ts): ST0RE does not + * yet expose a raw-bytes upload endpoint for 0pt to call, so the actual bytes + * are persisted by LocalBlobStore (interim) and only the resulting location + * is registered here as metadata. + * + * Convention (identical to aud0-client.ts / cl0ud-client.ts / n0d-client.ts / + * st0re-lakehouse-client.ts): when no base URL is configured the client + * returns a deterministic in-memory MOCK artifact (id prefixed + * "mock_artifact_") with NO network call, so unit tests stay hermetic. A real + * deployment sets ST0RE_URL to register against the live artifact registry. + */ +import crypto from "crypto"; + +export interface St0reObjectLocation { + provider: string; + bucket: string; + key: string; + region?: string; +} + +export interface CreateArtifactInput { + tenant_id: string; + project_id: string; + artifact_type: string; + app_id: string; + produced_by: string; + object_location: St0reObjectLocation; + content_type: string; + size_bytes: number; + checksum_sha256: string; + /** "none" | "low" | "medium" | "high" — kept as string to stay tolerant of ST0RE's own vocabulary. */ + pii_level: string; + retention_policy: string; + data_residency: string; + /** Risk level L0–L5 per the SIAX Cloud risk taxonomy. */ + risk_level: string; + correlation_id?: string; +} + +export interface St0reArtifact extends CreateArtifactInput { + artifact_id: string; + created_at: string; +} + +export interface St0reClient { + createArtifact(input: CreateArtifactInput): Promise; +} + +export function createSt0reClient(baseUrl?: string): St0reClient { + if (!baseUrl) return new MockSt0reClient(); + return new HttpSt0reClient(baseUrl); +} + +/** No ST0RE_URL configured: returns a deterministic mock artifact (see module doc comment). */ +class MockSt0reClient implements St0reClient { + async createArtifact(input: CreateArtifactInput): Promise { + return { + ...input, + artifact_id: `mock_artifact_${crypto.randomUUID()}`, + created_at: new Date().toISOString(), + }; + } +} + +class HttpSt0reClient implements St0reClient { + private readonly baseUrl: string; + + constructor(baseUrl: string) { + this.baseUrl = baseUrl.replace(/\/+$/, ""); + } + + async createArtifact(input: CreateArtifactInput): Promise { + const res = await fetch(`${this.baseUrl}/v1/st0re/artifacts`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(input), + }); + if (!res.ok) { + throw new Error(`ST0RE create-artifact error: ${res.status} ${await res.text().catch(() => "")}`); + } + return (await res.json()) as St0reArtifact; + } +} diff --git a/src/services/dataset-service.ts b/src/services/dataset-service.ts new file mode 100644 index 0000000..e15717a --- /dev/null +++ b/src/services/dataset-service.ts @@ -0,0 +1,112 @@ +/** + * src/services/dataset-service.ts + * ================================ + * Owns 0pt.dataset.schema.v1 — the registry of 0pt datasets (raw / enriched / + * curated / feature outputs). In-memory reference implementation (same + * convention as the other 0pt services in this repo; a real deployment backs + * this with `opt_datasets`). + * + * Consumed by: + * - dataset-iceberg-mapping-service.ts (getDataset -> does the dataset exist + * before we map it to an Iceberg table?) + * - iceberg-write-service.ts (getDataset -> guard the write path) + * + * Registering a dataset emits a best-effort AUD0 `0pt.dataset_registered` + * event; with no AUD0_URL configured that emission is a no-op (see + * aud0-client.ts), so this stays hermetic in tests and local dev. + */ +import { createAud0Client, type Aud0Client } from "../clients/aud0-client"; + +export type DatasetType = "raw" | "enriched" | "curated" | "feature" | "derived"; +export type DatasetStatus = "active" | "deprecated" | "archived"; + +export interface Dataset { + dataset_id: string; + tenant_id: string; + project_id: string; + name: string; + dataset_type: DatasetType; + description?: string; + status: DatasetStatus; + correlation_id?: string; + created_at: string; + updated_at?: string; + created_by?: string; +} + +export interface RegisterDatasetInput { + dataset_id: string; + tenant_id: string; + project_id: string; + name: string; + dataset_type: DatasetType; + description?: string; + status?: DatasetStatus; + correlation_id?: string; + created_by?: string; +} + +export class DatasetService { + private datasets: Map = new Map(); + private aud0: Aud0Client; + + constructor(aud0BaseUrl?: string) { + this.aud0 = createAud0Client(aud0BaseUrl); + } + + async registerDataset(input: RegisterDatasetInput): Promise { + if (this.datasets.has(input.dataset_id)) { + throw new Error(`Dataset ${input.dataset_id} already exists`); + } + if (!input.name) { + throw new Error("name is required"); + } + + const now = new Date().toISOString(); + const dataset: Dataset = { + dataset_id: input.dataset_id, + tenant_id: input.tenant_id, + project_id: input.project_id, + name: input.name, + dataset_type: input.dataset_type, + description: input.description, + status: input.status ?? "active", + correlation_id: input.correlation_id, + created_at: now, + created_by: input.created_by, + }; + this.datasets.set(dataset.dataset_id, dataset); + + await this.aud0.ingestEvent({ + event_id: `evt_0pt_dataset_registered_${dataset.dataset_id}`, + event_type: "0pt.dataset_registered", + tenant_id: dataset.tenant_id, + project_id: dataset.project_id, + actor_id: dataset.created_by ?? "system", + app_id: "0pt", + capability_id: "0pt.register_dataset", + resource_type: "dataset", + resource_id: dataset.dataset_id, + risk_level: "L2", + payload: { dataset_type: dataset.dataset_type, name: dataset.name }, + created_at: now, + correlation_id: dataset.correlation_id, + }); + + return dataset; + } + + async getDataset(datasetId: string): Promise { + const dataset = this.datasets.get(datasetId); + if (!dataset) throw new Error(`Dataset ${datasetId} not found`); + return dataset; + } + + async listDatasets(filter?: { tenant_id?: string; project_id?: string; dataset_type?: DatasetType }): Promise { + let results = Array.from(this.datasets.values()); + if (filter?.tenant_id) results = results.filter((d) => d.tenant_id === filter.tenant_id); + if (filter?.project_id) results = results.filter((d) => d.project_id === filter.project_id); + if (filter?.dataset_type) results = results.filter((d) => d.dataset_type === filter.dataset_type); + return results; + } +} diff --git a/src/services/source-service.ts b/src/services/source-service.ts new file mode 100644 index 0000000..a529d0f --- /dev/null +++ b/src/services/source-service.ts @@ -0,0 +1,85 @@ +/** + * src/services/source-service.ts + * =============================== + * Owns 0pt.source.schema.v1 — the registry of upstream data sources + * (databases, crawl domains, APIs, file drops). In-memory reference + * implementation (same convention as the other 0pt services in this repo: + * each API route file constructs its own instance; a real deployment backs + * this with `opt_sources` — sql/002_p0_dataset_quality.sql). + * + * Consumed by: + * - source-query-service.ts (getSource -> is this source queryable?) + * - source-rate-policy-service.ts (getSource -> does the source exist?) + * + * Registering a source emits a best-effort AUD0 `0pt.source_registered` + * event; with no AUD0_URL configured that emission is a no-op (see + * aud0-client.ts), so this stays hermetic in tests and local dev. + */ +import { createAud0Client, type Aud0Client } from "../clients/aud0-client"; +import type { CreateSourceInput, Source } from "../types/source"; + +export class SourceService { + private sources: Map = new Map(); + private aud0: Aud0Client; + + constructor(aud0BaseUrl?: string) { + this.aud0 = createAud0Client(aud0BaseUrl); + } + + async createSource(input: CreateSourceInput): Promise { + if (this.sources.has(input.source_id)) { + throw new Error(`Source ${input.source_id} already exists`); + } + if (!input.name) { + throw new Error("name is required"); + } + + const now = new Date().toISOString(); + const source: Source = { + source_id: input.source_id, + tenant_id: input.tenant_id, + project_id: input.project_id, + source_type: input.source_type, + name: input.name, + adapter_id: input.adapter_id, + description: input.description, + status: input.status ?? "active", + correlation_id: input.correlation_id, + created_at: now, + created_by: input.created_by, + }; + this.sources.set(source.source_id, source); + + await this.aud0.ingestEvent({ + event_id: `evt_0pt_source_registered_${source.source_id}`, + event_type: "0pt.source_registered", + tenant_id: source.tenant_id, + project_id: source.project_id, + actor_id: source.created_by ?? "system", + app_id: "0pt", + capability_id: "0pt.register_source", + resource_type: "source", + resource_id: source.source_id, + risk_level: "L2", + payload: { source_type: source.source_type, adapter_id: source.adapter_id ?? null, name: source.name }, + created_at: now, + correlation_id: source.correlation_id, + }); + + return source; + } + + async getSource(sourceId: string): Promise { + const source = this.sources.get(sourceId); + if (!source) throw new Error(`Source ${sourceId} not found`); + return source; + } + + async listSources(filter?: { tenant_id?: string; project_id?: string; source_type?: Source["source_type"] }): Promise { + let results = Array.from(this.sources.values()); + if (filter?.tenant_id) results = results.filter((s) => s.tenant_id === filter.tenant_id); + if (filter?.project_id) results = results.filter((s) => s.project_id === filter.project_id); + if (filter?.source_type) results = results.filter((s) => s.source_type === filter.source_type); + return results; + } +} diff --git a/src/types/source.ts b/src/types/source.ts new file mode 100644 index 0000000..1d515ac --- /dev/null +++ b/src/types/source.ts @@ -0,0 +1,45 @@ +/** + * src/types/source.ts + * ===================== + * 0pt.source.schema.v1 — a registered upstream data source (a database, a + * crawlable web domain, an API, a file drop, …). Owned by SourceService + * (src/services/source-service.ts); referenced by SourceRatePolicy + * (src/types/source-rate-policy.ts) and by the synchronous query path + * (src/services/source-query-service.ts), which reads `source_type` + + * `adapter_id` to decide whether a Source is queryable. + * + * `adapter_id` is the seam that lets a "database" source name the concrete + * driver 0pt should use for it (today only "fabric_onelake_sql" is wired on + * the synchronous path — see source-query-service.ts). + */ +export type SourceType = "database" | "file" | "web" | "api" | "stream" | "manual"; +export type SourceStatus = "active" | "paused" | "archived"; + +export interface Source { + source_id: string; + tenant_id: string; + project_id: string; + source_type: SourceType; + name: string; + /** Concrete driver id for this source, e.g. "fabric_onelake_sql". Optional: not every source is directly queryable. */ + adapter_id?: string; + description?: string; + status: SourceStatus; + correlation_id?: string; + created_at: string; + updated_at?: string; + created_by?: string; +} + +export interface CreateSourceInput { + source_id: string; + tenant_id: string; + project_id: string; + source_type: SourceType; + name: string; + adapter_id?: string; + description?: string; + status?: SourceStatus; + correlation_id?: string; + created_by?: string; +} diff --git a/tsconfig.check.json b/tsconfig.check.json new file mode 100644 index 0000000..8c1f266 --- /dev/null +++ b/tsconfig.check.json @@ -0,0 +1,10 @@ +{ + "//": "Scoped, blocking type-check for 0pt's capability code (src/ + tests/). The root tsconfig.json intentionally also covers the v0-generated Next.js UI (app/, components/, hooks/), whose known TS issues next.config.mjs suppresses via `typescript.ignoreBuildErrors: true`. This config type-checks the backend capability layer — the actual deliverable — under the SAME strict settings as the root tsconfig, without inheriting the UI's suppressed surface. Invoked by `pnpm run typecheck`.", + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "types": ["node"] + }, + "include": ["src/**/*.ts", "tests/**/*.ts"], + "exclude": ["node_modules", ".next"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..a99c130 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vitest/config"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +// `@/…` maps to the repo root, matching tsconfig.json's `paths` (`@/*` -> `./*`), +// so tests can `import { X } from "@/src/services/…"` exactly as the app code does. +const rootDir = path.dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + resolve: { + alias: { + "@": rootDir, + }, + }, + test: { + // P0 capability tests. Node environment (these are backend service/adapter + // tests, no DOM). The `.realinfra.` test self-skips when its live backend + // is unreachable — see tests/p0/iceberg-write-service.realinfra.test.ts. + include: ["tests/**/*.test.ts"], + environment: "node", + globals: false, + }, +}); -- 2.54.0 From 1d76d50f3d064f40d0b90923c62d0b6cb00206f2 Mon Sep 17 00:00:00 2001 From: SIAX Foundation Bot Date: Sat, 25 Jul 2026 14:32:34 +0200 Subject: [PATCH 3/3] feat(auth,foundation): Wave 2 flag-gated Zitadel JWT verify + validation + eslint + tests Co-Authored-By: Claude Opus 4.8 --- .env.example | 17 ++ .gitea/workflows/ci.yml | 52 ++--- .prettierrc.json | 6 + .../[documentId]/extraction/route.ts | 8 +- app/api/0pt/documents/capture/route.ts | 8 +- .../mappings/[mappingId]/writes/route.ts | 8 +- app/api/0pt/lakehouse/mappings/route.ts | 8 +- app/api/0pt/sources/[sourceId]/query/route.ts | 8 +- app/api/0pt/sources/rate-policies/route.ts | 8 +- middleware.ts | 55 +++++ package.json | 6 +- pnpm-lock.yaml | 18 ++ src/auth/context.ts | 188 ++++++++++++++++ src/auth/zitadel.ts | 213 ++++++++++++++++++ src/validation/0pt-schemas.ts | 123 ++++++++++ tests/auth/context.test.ts | 118 ++++++++++ tests/auth/zitadel.test.ts | 124 ++++++++++ tests/validation/0pt-schemas.test.ts | 142 ++++++++++++ 18 files changed, 1074 insertions(+), 36 deletions(-) create mode 100644 .prettierrc.json create mode 100644 middleware.ts create mode 100644 src/auth/context.ts create mode 100644 src/auth/zitadel.ts create mode 100644 src/validation/0pt-schemas.ts create mode 100644 tests/auth/context.test.ts create mode 100644 tests/auth/zitadel.test.ts create mode 100644 tests/validation/0pt-schemas.test.ts diff --git a/.env.example b/.env.example index 7653cd8..e4ee706 100644 --- a/.env.example +++ b/.env.example @@ -32,6 +32,23 @@ GRANT_ENC_KEY=change-me-32-byte-key # Shared token for internal/service-to-service calls. INTERNAL_TOKEN=change-me-internal-token +# ---- Zitadel / OIDC Bearer verification (Wave 2 hardening) ------------------ +# 0pt verifies inbound Bearer tokens against the fleet Zitadel IdP +# (src/auth/zitadel.ts, wired via middleware.ts). JWKS are discovered from +# /.well-known/openid-configuration, falling back to /oauth/v2/keys. +# +# Fleet Zitadel issuer (OIDC). Default when unset: https://id-customers.siax.io +ZITADEL_ISSUER=https://id-customers.siax.io +# Flag-gated enforcement. When != "true" (DEFAULT) auth is non-breaking: +# a present Bearer token is best-effort verified and attached, but never +# rejected. Set to "true" to REQUIRE a valid token on /api/0pt/* (401 on +# missing/invalid, 403 when X-Tenant-Id disagrees with the token tenant). +AUTH_ENFORCE=false +# Optional override for the claim key that carries the SIAX tenant id. When +# unset, tenant derivation uses the documented precedence: +# urn:zitadel:iam:org:id -> urn:zitadel:iam:user:resourceowner:id -> tenant_id -> tenant. +ZITADEL_TENANT_CLAIM= + # ---- Payments (Stripe) ------------------------------------------------------ STRIPE_SECRET_KEY=sk_test_xxx STRIPE_WEBHOOK_SECRET=whsec_xxx diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index ba24a43..45debd6 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -3,25 +3,19 @@ name: CI (SIAX Cloud) # Self-hosted CI on SIAX Cloud (Gitea Actions, runners siax-runner-1/2), # following the fleet-wide pattern (quality gate -> advisory security scan). # -# WHAT THIS REPO ACTUALLY SHIPS (0pt web console): a Next.js 16 app. Its -# package.json exposes exactly four scripts -- build / dev / lint / start -- -# so the BLOCKING quality gate below runs the one real, installed, functional -# build script (`pnpm run build` == `next build`), which type-checks and -# compiles the whole app. -# -# Deliberately NOT run here (honest gaps, additive Wave-1 scope only): -# * lint -> the "lint" script is `eslint .` but eslint is NOT in the -# dependency tree (zero eslint entries in pnpm-lock.yaml), so a -# blocking lint step would fail on "eslint: not found" for a -# missing-devDependency reason, not a code reason. Adding eslint -# is a source/package.json change (Wave 2/3), not Wave-1 -# scaffolding. Re-enable once eslint is a real devDependency. -# * typecheck-> no `typecheck` script exists, and next.config.mjs sets -# `typescript.ignoreBuildErrors: true` on purpose; `next build` -# already runs the TS compiler. -# * test -> tests/p0/*.test.ts exist but there is NO test runner installed -# (no vitest/jest in pnpm-lock.yaml) and no `test` script, so -# there is nothing to invoke. Wiring a runner is Wave 2/3. +# WHAT THIS REPO SHIPS: a Next.js 16 web console PLUS the 0pt capability layer +# (src/, tests/). Wave 2/3 hardening added a real test runner (vitest), a +# scoped strict type-check (tsconfig.check.json), and a flat ESLint config, so +# the quality gate now runs them: +# * typecheck (BLOCKING) -> `tsc -p tsconfig.check.json` over src/ + tests/. +# * test (BLOCKING) -> `vitest run` (the src/ capability + auth + validation suites). +# * lint (ADVISORY, continue-on-error) -> `eslint src tests`. The v0-generated +# legacy adapter/test tree carries pre-existing lint noise +# (explicit-any / require-style imports), so lint is advisory to +# avoid blocking merges on inherited debt; the Wave 2 auth/validation +# additions themselves are lint-clean. Flip to blocking once the +# legacy noise is cleared. +# * build (BLOCKING) -> `next build` compiles the whole app. # # The security job is intentionally advisory (separate job, `--exit-code 0`) so # it never blocks merges, matching the fleet convention. @@ -45,17 +39,17 @@ jobs: run: | corepack enable corepack prepare pnpm@9 --activate - # --ignore-workspace: the repo's pnpm-workspace.yaml is a v0-export artifact - # that carries only an `allowBuilds:` block and NO `packages:` field, which - # makes pnpm abort with "packages field missing or empty". There are no real - # workspace members (only the root package.json), so ignoring the workspace - # file is a semantic no-op and lets the frozen install resolve cleanly. - name: Install (frozen lockfile) - run: pnpm install --frozen-lockfile --ignore-workspace - - name: Build (next build -- blocking quality gate, no `|| true`) - # --ignore-workspace: `pnpm run` also re-parses pnpm-workspace.yaml and aborts - # on the missing `packages:` field unless the workspace is ignored (see above). - run: pnpm --ignore-workspace run build + run: pnpm install --frozen-lockfile + - name: Typecheck (tsc over src/ + tests/ -- blocking) + run: pnpm run typecheck + - name: Test (vitest -- blocking) + run: pnpm run test + - name: Lint (eslint -- advisory; legacy tree has pre-existing noise) + continue-on-error: true + run: pnpm run lint + - name: Build (next build -- blocking quality gate) + run: pnpm run build security: runs-on: ubuntu-latest diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..f423e70 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 110 +} diff --git a/app/api/0pt/documents/[documentId]/extraction/route.ts b/app/api/0pt/documents/[documentId]/extraction/route.ts index 746594f..2d6a9b1 100644 --- a/app/api/0pt/documents/[documentId]/extraction/route.ts +++ b/app/api/0pt/documents/[documentId]/extraction/route.ts @@ -1,10 +1,16 @@ import { NextRequest, NextResponse } from "next/server"; +import { parseBody, documentExtractionSchema } from "@/src/validation/0pt-schemas"; import { documentExtractionService } from "@/src/services/document-extraction-service"; export async function POST(request: NextRequest, { params }: { params: Promise<{ documentId: string }> }) { try { const { documentId } = await params; - const body = await request.json(); + const __raw = await request.json(); + const __parsed = parseBody(documentExtractionSchema, __raw); + if (!__parsed.ok) { + return NextResponse.json(__parsed.body, { status: __parsed.status }); + } + const body = __parsed.data; const result = await documentExtractionService.extractDocument(documentId, { tenant_id: body.tenant_id, project_id: body.project_id, diff --git a/app/api/0pt/documents/capture/route.ts b/app/api/0pt/documents/capture/route.ts index 619cc15..f2f0124 100644 --- a/app/api/0pt/documents/capture/route.ts +++ b/app/api/0pt/documents/capture/route.ts @@ -1,9 +1,15 @@ import { NextRequest, NextResponse } from "next/server"; +import { parseBody, documentCaptureSchema } from "@/src/validation/0pt-schemas"; import { documentCaptureService } from "@/src/services/document-capture-service"; export async function POST(request: NextRequest) { try { - const body = await request.json(); + const __raw = await request.json(); + const __parsed = parseBody(documentCaptureSchema, __raw); + if (!__parsed.ok) { + return NextResponse.json(__parsed.body, { status: __parsed.status }); + } + const body = __parsed.data; const result = await documentCaptureService.captureFromMailbox( { mailbox: { diff --git a/app/api/0pt/lakehouse/mappings/[mappingId]/writes/route.ts b/app/api/0pt/lakehouse/mappings/[mappingId]/writes/route.ts index 52fccdd..2113bc7 100644 --- a/app/api/0pt/lakehouse/mappings/[mappingId]/writes/route.ts +++ b/app/api/0pt/lakehouse/mappings/[mappingId]/writes/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { parseBody, requestIcebergWriteSchema } from "@/src/validation/0pt-schemas"; import { IcebergWriteService } from "@/src/services/iceberg-write-service"; const writeService = new IcebergWriteService(); @@ -8,7 +9,12 @@ const writeService = new IcebergWriteService(); export async function POST(request: NextRequest, { params }: { params: Promise<{ mappingId: string }> }) { try { const { mappingId } = await params; - const body = await request.json(); + const __raw = await request.json(); + const __parsed = parseBody(requestIcebergWriteSchema, __raw); + if (!__parsed.ok) { + return NextResponse.json(__parsed.body, { status: __parsed.status }); + } + const body = __parsed.data; const result = await writeService.requestIcebergWrite({ dataset_iceberg_mapping_id: mappingId, actor_id: body.actor_id, diff --git a/app/api/0pt/lakehouse/mappings/route.ts b/app/api/0pt/lakehouse/mappings/route.ts index a843b54..c217cd1 100644 --- a/app/api/0pt/lakehouse/mappings/route.ts +++ b/app/api/0pt/lakehouse/mappings/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { parseBody, mapDatasetToIcebergTableSchema } from "@/src/validation/0pt-schemas"; import { IcebergWriteService } from "@/src/services/iceberg-write-service"; import { DatasetIcebergMappingService } from "@/src/services/dataset-iceberg-mapping-service"; @@ -16,7 +17,12 @@ const writeService = new IcebergWriteService({ mappingService }); // entrypoint, see docs/ICEBERG_WRITE_BOUNDARY.md. export async function POST(request: NextRequest) { try { - const body = await request.json(); + const __raw = await request.json(); + const __parsed = parseBody(mapDatasetToIcebergTableSchema, __raw); + if (!__parsed.ok) { + return NextResponse.json(__parsed.body, { status: __parsed.status }); + } + const body = __parsed.data; const result = await writeService.mapDatasetToIcebergTable({ tenant_id: body.tenant_id, project_id: body.project_id, diff --git a/app/api/0pt/sources/[sourceId]/query/route.ts b/app/api/0pt/sources/[sourceId]/query/route.ts index f1b5d0f..038098f 100644 --- a/app/api/0pt/sources/[sourceId]/query/route.ts +++ b/app/api/0pt/sources/[sourceId]/query/route.ts @@ -15,6 +15,7 @@ * Details: docs/P0_QUERY_SOURCE_FLOW.md. */ import { NextRequest, NextResponse } from "next/server"; +import { parseBody, sourceQuerySchema } from "@/src/validation/0pt-schemas"; import { SourceService } from "@/src/services/source-service"; import { SourceQueryService, SourceQueryTimeoutError, SourceQueryUnsupportedError } from "@/src/services/source-query-service"; @@ -24,7 +25,12 @@ const queryService = new SourceQueryService({ sourceService }); export async function POST(request: NextRequest, { params }: { params: Promise<{ sourceId: string }> }) { try { const { sourceId } = await params; - const body = await request.json(); + const __raw = await request.json(); + const __parsed = parseBody(sourceQuerySchema, __raw); + if (!__parsed.ok) { + return NextResponse.json(__parsed.body, { status: __parsed.status }); + } + const body = __parsed.data; if (typeof body.query !== "string" || body.query.trim().length === 0) { return NextResponse.json({ error: "query (string) is required" }, { status: 400 }); diff --git a/app/api/0pt/sources/rate-policies/route.ts b/app/api/0pt/sources/rate-policies/route.ts index cce357c..caa8607 100644 --- a/app/api/0pt/sources/rate-policies/route.ts +++ b/app/api/0pt/sources/rate-policies/route.ts @@ -1,11 +1,17 @@ import { NextRequest, NextResponse } from "next/server"; +import { parseBody, createSourceRatePolicySchema } from "@/src/validation/0pt-schemas"; import { SourceRatePolicyService } from "@/src/services/source-rate-policy-service"; const service = new SourceRatePolicyService(); export async function POST(request: NextRequest) { try { - const body = await request.json(); + const __raw = await request.json(); + const __parsed = parseBody(createSourceRatePolicySchema, __raw); + if (!__parsed.ok) { + return NextResponse.json(__parsed.body, { status: __parsed.status }); + } + const body = __parsed.data; const policy = await service.createSourceRatePolicy(body); return NextResponse.json(policy, { status: 201 }); } catch (error: unknown) { diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..d551401 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,55 @@ +/** + * `middleware.ts` -- Next.js request middleware = 0pt's framework-level auth + * seam (the Next analog of a Fastify `onRequest` hook). + * + * Wave 2 hardening: this runs the flag-gated Zitadel verification + * (src/auth/context.ts -> src/auth/zitadel.ts) in front of every 0pt + * capability route (/api/0pt/*). It does NOT touch the v0-generated UI routes + * or the unrelated payment/order APIs. + * + * AUTH_ENFORCE != "true" (default): never rejects; if a verified token is + * present the verified principal is forwarded to the handler as + * `x-0pt-verified-sub` / `x-0pt-verified-tenant` request headers. + * AUTH_ENFORCE == "true": a valid verified token is required; 401 on + * missing/invalid, 403 when X-Tenant-Id disagrees with the token tenant. + * + * Health/metrics are exempt (they are also outside the matcher below, but the + * exemption is kept explicit for robustness). + */ + +import { NextResponse, type NextRequest } from 'next/server'; +import { resolveAuthContext } from '@/src/auth/context'; + +// Only guard the 0pt capability surface. The matcher keeps this middleware off +// the UI, auth, and payment routes entirely. +export const config = { + matcher: ['/api/0pt/:path*'], +}; + +const EXEMPT_PATTERNS: RegExp[] = [/^\/api\/health(\/|$)/, /^\/api\/admin\/metrics(\/|$)/]; + +export async function middleware(req: NextRequest): Promise { + const path = req.nextUrl.pathname; + if (EXEMPT_PATTERNS.some((re) => re.test(path))) { + return NextResponse.next(); + } + + const decision = await resolveAuthContext(req.headers); + + if (!decision.ok) { + return NextResponse.json( + { error: decision.code, message: decision.message, ...(decision.details ? { details: decision.details } : {}) }, + { status: decision.status }, + ); + } + + const principal = decision.ctx.principal; + if (principal) { + const forwarded = new Headers(req.headers); + forwarded.set('x-0pt-verified-sub', principal.sub); + forwarded.set('x-0pt-verified-tenant', principal.tenantId); + return NextResponse.next({ request: { headers: forwarded } }); + } + + return NextResponse.next(); +} diff --git a/package.json b/package.json index 0f5de3f..edfa5b3 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "start": "next start", "typecheck": "tsc -p tsconfig.check.json", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "format": "prettier --check .", + "format:write": "prettier --write ." }, "dependencies": { "@hookform/resolvers": "^3.10.0", @@ -49,6 +51,7 @@ "date-fns": "4.1.0", "embla-carousel-react": "8.5.1", "input-otp": "1.4.1", + "jose": "^5.9.6", "lucide-react": "^0.454.0", "next": "16.0.10", "next-themes": "^0.4.6", @@ -74,6 +77,7 @@ "@types/react-dom": "^19", "eslint": "^9.15.0", "postcss": "^8.5", + "prettier": "^3.9.6", "tailwindcss": "^4.1.9", "tw-animate-css": "1.3.3", "typescript": "^5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 511a8e6..a2e1bf8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,6 +116,9 @@ importers: input-otp: specifier: 1.4.1 version: 1.4.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + jose: + specifier: ^5.9.6 + version: 5.10.0 lucide-react: specifier: ^0.454.0 version: 0.454.0(react@19.2.0) @@ -186,6 +189,9 @@ importers: postcss: specifier: ^8.5 version: 8.5.15 + prettier: + specifier: ^3.9.6 + version: 3.9.6 tailwindcss: specifier: ^4.1.9 version: 4.3.1 @@ -2068,6 +2074,9 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2299,6 +2308,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -4407,6 +4421,8 @@ snapshots: jiti@2.7.0: {} + jose@5.10.0: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -4598,6 +4614,8 @@ snapshots: prelude-ls@1.2.1: {} + prettier@3.9.6: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 diff --git a/src/auth/context.ts b/src/auth/context.ts new file mode 100644 index 0000000..1a19091 --- /dev/null +++ b/src/auth/context.ts @@ -0,0 +1,188 @@ +/** + * `0pt/auth/context.ts` -- request-context extraction AND auth, the single + * seam the framework layer (Next.js middleware.ts) calls. + * + * ========================================================================== + * WAVE 2 HARDENING STATUS + * + * 0pt now performs REAL, cryptographic Zitadel (OIDC) Bearer-token + * verification (src/auth/zitadel.ts), gated behind the AUTH_ENFORCE env flag + * so it can ship to the deployed console WITHOUT breaking existing callers: + * + * AUTH_ENFORCE != "true" (DEFAULT, non-breaking): + * Preserves the historical behavior (no token was ever required; tenant + * came from the X-Tenant-Id header / request body). ADDITIONALLY, if a + * Bearer token is present and JWT-shaped, we ATTEMPT `verifyBearer` and + * attach the verified principal to the resolved context -- but we NEVER + * reject on failure in this mode (a warning is logged). This lets tokens + * start flowing and being verified with zero caller migration. + * + * AUTH_ENFORCE == "true" (enforced): + * REQUIRES a valid, verified token on every non-exempt route. The tenant + * is derived from the TOKEN (not the X-Tenant-Id header). Missing/invalid + * token -> 401 (OPT_UNAUTHENTICATED). If an X-Tenant-Id header is present + * and disagrees with the token's tenant -> 403 (OPT_ACCESS_DENIED). + * + * This module is framework-neutral: it takes a minimal header-lookup (the Web + * `Headers` interface satisfies it, as does Next's `NextRequest.headers`) and + * returns a decision object. It does NOT import next/*, so it is fully + * unit-testable in isolation. + * ========================================================================== + */ + +import { verifyBearer, type VerifiedPrincipal } from './zitadel'; + +/** Minimal header accessor -- Web `Headers` and `NextRequest.headers` satisfy this. */ +export interface HeaderLookup { + get(name: string): string | null | undefined; +} + +export interface OptRequestContext { + /** Tenant id: from the token in enforce mode, else from X-Tenant-Id header. */ + tenantId: string; + /** Actor id: from X-Actor-Id header, falling back to the token subject. */ + actorId: string; + /** Correlation/request id (X-Request-Id header, else a fresh uuid). */ + requestId: string; + /** The raw bearer token, if an Authorization: Bearer header was present. */ + bearerToken?: string; + /** Present when a Bearer token was successfully verified (either mode). */ + principal?: VerifiedPrincipal; +} + +export type AuthDecision = + | { ok: true; ctx: OptRequestContext } + | { + ok: false; + status: 401 | 403; + code: 'OPT_UNAUTHENTICATED' | 'OPT_ACCESS_DENIED'; + message: string; + details?: Record; + ctx: OptRequestContext; + }; + +/** True when hard auth enforcement is switched on (AUTH_ENFORCE=="true"). */ +export function authEnforced(): boolean { + return process.env.AUTH_ENFORCE === 'true'; +} + +function firstHeader(headers: HeaderLookup, name: string): string | undefined { + const v = headers.get(name); + if (v === null || v === undefined) return undefined; + return v; +} + +/** A raw token that structurally looks like a compact JWS (three segments). */ +function looksLikeJwt(token: string): boolean { + return token.split('.').length === 3; +} + +function newRequestId(): string { + try { + return globalThis.crypto.randomUUID(); + } catch { + return `req_${Date.now()}_${Math.random().toString(36).slice(2)}`; + } +} + +/** Read whatever context headers are present, without enforcing required-ness. */ +export function readRequestContext(headers: HeaderLookup): OptRequestContext { + const auth = firstHeader(headers, 'authorization'); + const bearerToken = auth?.startsWith('Bearer ') ? auth.slice('Bearer '.length).trim() : undefined; + return { + tenantId: firstHeader(headers, 'x-tenant-id') ?? '', + actorId: firstHeader(headers, 'x-actor-id') ?? '', + requestId: firstHeader(headers, 'x-request-id') ?? newRequestId(), + bearerToken: bearerToken && bearerToken.length > 0 ? bearerToken : undefined, + }; +} + +/** Minimal logger seam so middleware can pass the platform logger; defaults to console. */ +export interface AuthLogger { + warn(obj: unknown, msg?: string): void; +} + +export interface ResolveAuthOptions { + /** Injectable verifier (test seam); defaults to the real {@link verifyBearer}. */ + verify?: typeof verifyBearer; + /** Force enforcement on/off (test seam); defaults to {@link authEnforced}. */ + enforce?: boolean; + logger?: AuthLogger; +} + +/** + * Resolve and (in enforce mode) enforce the request auth context. Returns an + * {@link AuthDecision}; the caller (middleware) turns a `!ok` decision into an + * HTTP response and an `ok` decision into forwarded principal headers. + */ +export async function resolveAuthContext( + headers: HeaderLookup, + opts: ResolveAuthOptions = {}, +): Promise { + const verify = opts.verify ?? verifyBearer; + const enforce = opts.enforce ?? authEnforced(); + const logger: AuthLogger = opts.logger ?? { + warn: (o, m) => console.warn(m ?? 'auth warning', o), + }; + const ctx = readRequestContext(headers); + + if (enforce) { + // --- ENFORCED: a valid verified token is mandatory ---------------------- + if (!ctx.bearerToken) { + return { + ok: false, + status: 401, + code: 'OPT_UNAUTHENTICATED', + message: 'Authentication required: missing Authorization: Bearer ', + ctx, + }; + } + let principal: VerifiedPrincipal; + try { + principal = await verify(ctx.bearerToken); + } catch (err) { + return { + ok: false, + status: 401, + code: 'OPT_UNAUTHENTICATED', + message: 'Authentication required: invalid or unverifiable Bearer token', + details: { reason: err instanceof Error ? err.message : 'verification_failed' }, + ctx, + }; + } + // Tenant is derived from the TOKEN. A disagreeing X-Tenant-Id is a 403. + const headerTenant = firstHeader(headers, 'x-tenant-id'); + if (headerTenant && headerTenant !== principal.tenantId) { + return { + ok: false, + status: 403, + code: 'OPT_ACCESS_DENIED', + message: 'X-Tenant-Id header disagrees with authenticated token tenant', + details: { header_tenant: headerTenant, token_tenant: principal.tenantId }, + ctx, + }; + } + ctx.tenantId = principal.tenantId; + ctx.actorId = ctx.actorId || principal.sub; + ctx.principal = principal; + return { ok: true, ctx }; + } + + // --- NON-ENFORCE (default): never reject, best-effort verify -------------- + if (ctx.bearerToken && looksLikeJwt(ctx.bearerToken)) { + try { + const principal = await verify(ctx.bearerToken); + ctx.principal = principal; + // Do NOT overwrite the header/body tenant in non-enforce mode; only + // fill it in when the caller supplied none, to stay non-breaking. + if (!ctx.tenantId) ctx.tenantId = principal.tenantId; + if (!ctx.actorId) ctx.actorId = principal.sub; + } catch (err) { + logger.warn( + { err: err instanceof Error ? err.message : String(err) }, + '0pt: bearer verification failed (AUTH_ENFORCE off, ignoring)', + ); + } + } + return { ok: true, ctx }; +} diff --git a/src/auth/zitadel.ts b/src/auth/zitadel.ts new file mode 100644 index 0000000..25b4e86 --- /dev/null +++ b/src/auth/zitadel.ts @@ -0,0 +1,213 @@ +/** + * `0pt/auth/zitadel.ts` -- self-contained Zitadel (OIDC) Bearer JWT + * verification for 0pt. + * + * ========================================================================== + * Wave 2 foundational hardening: REAL cryptographic verification that closes + * the "no signature verification at all" hole (0pt's API layer historically + * trusted an X-Tenant-Id header and NEVER verified the Bearer token). This + * mirrors the canonical implementation already shipped on the n0d/aud0/api0/ + * inf0 hardening branches. It is intentionally self-contained (no dependency + * on @siax/capability-registry, which is not consumable cross-repo) and + * depends only on `jose` + the platform's global `fetch`. + * + * ISSUER / JWKS DISCOVERY + * ---------------------- + * The fleet IdP is Zitadel at ZITADEL_ISSUER (default + * https://id-customers.siax.io). Signing keys are resolved by: + * 1. Fetching `/.well-known/openid-configuration` and reading its + * `jwks_uri` (the spec-correct OIDC discovery path). + * 2. Falling back to `/oauth/v2/keys` (Zitadel's well-known JWKS + * endpoint) if discovery is unavailable. + * The resolved JWKS is wrapped in jose's `createRemoteJWKSet`, which caches + * keys and refreshes on an unknown `kid`; the resolver is additionally cached + * per-issuer for the process lifetime. + * + * TENANT CLAIM DERIVATION (documented precedence) + * ----------------------------------------------- + * `deriveTenantId` maps a verified token to a SIAX tenant id with this + * precedence (overridable via ZITADEL_TENANT_CLAIM, which -- when set and + * present on the token -- wins outright): + * 1. `urn:zitadel:iam:org:id` -- Zitadel's standard + * organization-id claim; + * 0pt's tenant boundary + * maps 1:1 to a Zitadel org. + * 2. `urn:zitadel:iam:user:resourceowner:id` -- the user's resource-owner + * org id (present on some + * token profiles). + * 3. `tenant_id` -- an explicit custom claim, + * if a Zitadel action/mapper + * injects the SIAX tenant + * slug directly. + * 4. `tenant` -- a looser custom fallback. + * A signed-but-tenantless token is not usable here and fails verification. + */ + +import { + createRemoteJWKSet, + jwtVerify, + type JWTPayload, + type JWTVerifyGetKey, +} from 'jose'; + +/** Default fleet Zitadel issuer (OIDC). Overridable via ZITADEL_ISSUER. */ +export const DEFAULT_ZITADEL_ISSUER = 'https://id-customers.siax.io'; + +/** Standard + custom claim paths tenant derivation reads, in precedence order. */ +export const ZITADEL_ORG_ID_CLAIM = 'urn:zitadel:iam:org:id'; +export const ZITADEL_RESOURCEOWNER_CLAIM = 'urn:zitadel:iam:user:resourceowner:id'; +export const TENANT_ID_CLAIM = 'tenant_id'; +export const TENANT_CLAIM = 'tenant'; + +/** Ordered claim keys checked by {@link deriveTenantId} (after any override). */ +export const TENANT_CLAIM_PRECEDENCE = [ + ZITADEL_ORG_ID_CLAIM, + ZITADEL_RESOURCEOWNER_CLAIM, + TENANT_ID_CLAIM, + TENANT_CLAIM, +] as const; + +/** Resolve the configured issuer, trimming and falling back to the default. */ +export function zitadelIssuer(): string { + const fromEnv = process.env.ZITADEL_ISSUER?.trim(); + return fromEnv && fromEnv.length > 0 ? fromEnv : DEFAULT_ZITADEL_ISSUER; +} + +/** The env-configured tenant-claim override, if any (ZITADEL_TENANT_CLAIM). */ +export function tenantClaimOverride(): string | undefined { + const v = process.env.ZITADEL_TENANT_CLAIM?.trim(); + return v && v.length > 0 ? v : undefined; +} + +/** A verified caller principal derived from a valid Zitadel access/ID token. */ +export interface VerifiedPrincipal { + /** The token subject (`sub`) -- the authenticated actor id. */ + sub: string; + /** The SIAX tenant id derived from the token (see deriveTenantId). */ + tenantId: string; + /** The full verified JWT payload, for any additional downstream claims. */ + claims: JWTPayload; +} + +/** Thrown by {@link verifyBearer} on any verification/derivation failure. */ +export class BearerVerificationError extends Error { + readonly reason: string; + readonly cause?: unknown; + constructor(reason: string, cause?: unknown) { + super(reason); + this.name = 'BearerVerificationError'; + this.reason = reason; + this.cause = cause; + } +} + +function claimAsString(claims: JWTPayload, key: string): string | undefined { + const v = claims[key]; + return typeof v === 'string' && v.length > 0 ? v : undefined; +} + +/** + * Map a verified JWT payload to a SIAX tenant id (documented precedence above). + * @param override an explicit claim key to try first (ZITADEL_TENANT_CLAIM). + */ +export function deriveTenantId(claims: JWTPayload, override = tenantClaimOverride()): string | undefined { + if (override) { + const fromOverride = claimAsString(claims, override); + if (fromOverride) return fromOverride; + } + for (const key of TENANT_CLAIM_PRECEDENCE) { + const v = claimAsString(claims, key); + if (v) return v; + } + return undefined; +} + +// --- JWKS resolution -------------------------------------------------------- +const jwksCache = new Map(); +// Test seam: an injected resolver bypasses remote discovery entirely. +let keyResolverOverride: JWTVerifyGetKey | undefined; + +/** + * TEST SEAM: inject a JWKS resolver (e.g. jose `createLocalJWKSet`) so unit + * tests can verify locally-signed tokens without any network access. Pass + * `undefined` to clear. + */ +export function __setKeyResolverForTests(resolver: JWTVerifyGetKey | undefined): void { + keyResolverOverride = resolver; +} + +/** TEST SEAM: clear the per-issuer remote-JWKS cache. */ +export function __resetJwksCache(): void { + jwksCache.clear(); +} + +function stripTrailingSlash(url: string): string { + return url.replace(/\/+$/, ''); +} + +async function resolveJwksUri(issuer: string): Promise { + const base = stripTrailingSlash(issuer); + const wellKnown = `${base}/.well-known/openid-configuration`; + try { + const res = await fetch(wellKnown); + if (res.ok) { + const cfg = (await res.json()) as { jwks_uri?: unknown }; + if (typeof cfg.jwks_uri === 'string' && cfg.jwks_uri.length > 0) { + return cfg.jwks_uri; + } + } + } catch { + // fall through to the Zitadel default keys endpoint + } + return `${base}/oauth/v2/keys`; +} + +async function getRemoteJwks(issuer: string): Promise { + const cached = jwksCache.get(issuer); + if (cached) return cached; + const uri = await resolveJwksUri(issuer); + const resolver = createRemoteJWKSet(new URL(uri)); + jwksCache.set(issuer, resolver); + return resolver; +} + +export interface VerifyBearerOptions { + /** Override the issuer (default: {@link zitadelIssuer}). */ + issuer?: string; + /** Override the key resolver (test seam; default: remote JWKS for issuer). */ + keyResolver?: JWTVerifyGetKey; + /** Override the tenant-claim key (default: {@link tenantClaimOverride}). */ + tenantClaim?: string; +} + +/** + * Verify a raw Bearer token string against Zitadel: checks the RS/ES + * signature (via JWKS), the `iss` claim, and `exp`/`nbf` (jose enforces + * expiry automatically), then derives the tenant id. Returns a + * {@link VerifiedPrincipal} or throws {@link BearerVerificationError}. + */ +export async function verifyBearer( + token: string, + opts: VerifyBearerOptions = {}, +): Promise { + const issuer = opts.issuer ?? zitadelIssuer(); + const resolver = opts.keyResolver ?? keyResolverOverride ?? (await getRemoteJwks(issuer)); + + let payload: JWTPayload; + try { + const result = await jwtVerify(token, resolver, { issuer }); + payload = result.payload; + } catch (err) { + throw new BearerVerificationError('token_verification_failed', err); + } + + const sub = typeof payload.sub === 'string' ? payload.sub : undefined; + if (!sub || sub.length === 0) { + throw new BearerVerificationError('token_missing_sub'); + } + const tenantId = deriveTenantId(payload, opts.tenantClaim ?? tenantClaimOverride()); + if (!tenantId) { + throw new BearerVerificationError('token_missing_tenant_claim'); + } + return { sub, tenantId, claims: payload }; +} diff --git a/src/validation/0pt-schemas.ts b/src/validation/0pt-schemas.ts new file mode 100644 index 0000000..5578681 --- /dev/null +++ b/src/validation/0pt-schemas.ts @@ -0,0 +1,123 @@ +/** + * `0pt/validation/0pt-schemas.ts` -- Wave 2 input validation for the 0pt + * capability API's MUTATING routes. + * + * Before Wave 2 every POST/PUT handler did `const body = await request.json()` + * and read fields straight off `body` (an implicit `any`), so a malformed + * payload reached the service layer untyped and surfaced as an opaque 500/400. + * These Zod schemas validate the request body at the edge and produce a typed, + * structured 400. This is VALIDATION ONLY -- it re-encodes the shape each + * handler already destructures; it does not add or change business logic. + */ + +import { z } from 'zod'; + +/** A structured 400 parse result. */ +export type ParseResult = + | { ok: true; data: T } + | { ok: false; status: 400; body: { error: 'validation_error'; issues: { path: string; message: string }[] } }; + +/** Validate `body` against `schema`, returning parsed data or a 400 payload. */ +export function parseBody(schema: S, body: unknown): ParseResult> { + const result = schema.safeParse(body); + if (result.success) return { ok: true, data: result.data }; + return { + ok: false, + status: 400, + body: { + error: 'validation_error', + issues: result.error.issues.map((i) => ({ + path: i.path.join('.'), + message: i.message, + })), + }, + }; +} + +// A permissive JSON object (used for opaque config blobs whose inner shape is +// owned by a downstream adapter, not this validation layer). +const jsonObject = z.record(z.string(), z.unknown()); + +/** POST /api/0pt/documents/capture */ +export const documentCaptureSchema = z.object({ + mailbox: z.object({ + tenant_id: z.string().min(1), + client_id: z.string().min(1), + client_secret: z.string().min(1), + mailbox_address: z.string().min(1), + folder: z.string().min(1).optional(), + }), + since_iso: z.string().min(1).optional(), + top: z.number().int().positive().optional(), + max_pages: z.number().int().positive().optional(), + tenant_id: z.string().min(1), + project_id: z.string().min(1), + actor_id: z.string().min(1).optional(), + correlation_id: z.string().min(1).optional(), +}); + +/** POST /api/0pt/documents/[documentId]/extraction */ +export const documentExtractionSchema = z.object({ + tenant_id: z.string().min(1), + project_id: z.string().min(1), + actor_id: z.string().min(1).optional(), + correlation_id: z.string().min(1).optional(), + model_id: z.string().min(1).optional(), +}); + +/** POST /api/0pt/sources/[sourceId]/query */ +export const sourceQuerySchema = z.object({ + query: z.string().trim().min(1), + params: z.unknown().optional(), + tenant_id: z.string().min(1), + project_id: z.string().min(1), + actor_id: z.string().min(1).optional(), + correlation_id: z.string().min(1).optional(), + max_rows: z.number().int().positive().optional(), + timeout_ms: z.number().int().positive().optional(), + connection_config: jsonObject, +}); + +/** POST /api/0pt/sources/rate-policies -- mirrors CreateSourceRatePolicyInput. */ +export const createSourceRatePolicySchema = z.object({ + source_rate_policy_id: z.string().min(1), + tenant_id: z.string().min(1), + project_id: z.string().min(1), + source_id: z.string().min(1), + rate_limit_per_minute: z.number().int().nonnegative(), + crawl_budget: z.number().int().nonnegative(), + compliance_notes: z.string().optional(), + domain: z.string().min(1).optional(), + robots_ok: z.boolean().optional(), + access: z.enum(['api', 'bulk', 'authorized_web', 'licensed']).optional(), + status: z.enum(['active', 'paused', 'deprecated']).optional(), + correlation_id: z.string().min(1).optional(), + created_by: z.string().min(1).optional(), +}); + +/** POST /api/0pt/lakehouse/mappings -- mirrors MapDatasetToIcebergTableInput. */ +export const mapDatasetToIcebergTableSchema = z.object({ + tenant_id: z.string().min(1), + project_id: z.string().min(1), + dataset_id: z.string().min(1), + dataset_iceberg_mapping_id: z.string().min(1), + namespace: z.string().min(1), + table_name: z.string().min(1), + storage_uri: z.string().min(1), + catalog_ref: z.string().min(1), + table_format: z.string().min(1).optional(), + actor_id: z.string().min(1), + correlation_id: z.string().min(1), +}); + +/** POST /api/0pt/lakehouse/mappings/[mappingId]/writes -- RequestIcebergWriteInput. */ +export const requestIcebergWriteSchema = z.object({ + actor_id: z.string().min(1), + correlation_id: z.string().min(1), + snapshot_type: z.string().min(1).optional(), + schema_version: z.string().min(1), + manifest_uri: z.string().min(1), + record_count: z.number().int().nonnegative().optional(), + size_bytes: z.number().int().nonnegative().optional(), + heavy: z.boolean().optional(), +}); diff --git a/tests/auth/context.test.ts b/tests/auth/context.test.ts new file mode 100644 index 0000000..2ee0c6e --- /dev/null +++ b/tests/auth/context.test.ts @@ -0,0 +1,118 @@ +/** + * Unit tests for src/auth/context.ts `resolveAuthContext` -- the flag-gated + * auth seam. Verification is injected (opts.verify) so these tests are pure + * and network-free; they exercise the decision logic in both AUTH_ENFORCE + * modes. + */ + +import { describe, it, expect, vi } from 'vitest'; +import { resolveAuthContext, type OptRequestContext } from '@/src/auth/context'; +import type { VerifiedPrincipal } from '@/src/auth/zitadel'; + +const principal: VerifiedPrincipal = { + sub: 'user_1', + tenantId: 'tenant_x', + claims: { sub: 'user_1', iss: 'https://id-customers.siax.io' }, +}; + +const okVerify = vi.fn(async () => principal); +const failVerify = vi.fn(async () => { + throw new Error('token_verification_failed'); +}); + +function h(init: Record): Headers { + return new Headers(init); +} + +describe('resolveAuthContext -- enforce mode', () => { + it('401s when no bearer token is present', async () => { + const d = await resolveAuthContext(h({ 'x-tenant-id': 'tenant_x' }), { enforce: true, verify: okVerify }); + expect(d.ok).toBe(false); + if (!d.ok) { + expect(d.status).toBe(401); + expect(d.code).toBe('OPT_UNAUTHENTICATED'); + } + }); + + it('401s when the token fails verification', async () => { + const d = await resolveAuthContext(h({ authorization: 'Bearer bad.tok.en' }), { + enforce: true, + verify: failVerify, + }); + expect(d.ok).toBe(false); + if (!d.ok) expect(d.status).toBe(401); + }); + + it('attaches the verified principal and derives tenant from the TOKEN', async () => { + const d = await resolveAuthContext(h({ authorization: 'Bearer good.tok.en' }), { + enforce: true, + verify: okVerify, + }); + expect(d.ok).toBe(true); + if (d.ok) { + expect(d.ctx.principal).toEqual(principal); + expect(d.ctx.tenantId).toBe('tenant_x'); + expect(d.ctx.actorId).toBe('user_1'); + } + }); + + it('403s when X-Tenant-Id disagrees with the token tenant', async () => { + const d = await resolveAuthContext( + h({ authorization: 'Bearer good.tok.en', 'x-tenant-id': 'tenant_evil' }), + { enforce: true, verify: okVerify }, + ); + expect(d.ok).toBe(false); + if (!d.ok) { + expect(d.status).toBe(403); + expect(d.code).toBe('OPT_ACCESS_DENIED'); + expect(d.details).toMatchObject({ header_tenant: 'tenant_evil', token_tenant: 'tenant_x' }); + } + }); + + it('allows a matching X-Tenant-Id', async () => { + const d = await resolveAuthContext( + h({ authorization: 'Bearer good.tok.en', 'x-tenant-id': 'tenant_x' }), + { enforce: true, verify: okVerify }, + ); + expect(d.ok).toBe(true); + }); +}); + +describe('resolveAuthContext -- non-enforce (default) mode', () => { + it('never rejects when no token is present', async () => { + const d = await resolveAuthContext(h({ 'x-tenant-id': 'tenant_header' }), { + enforce: false, + verify: okVerify, + }); + expect(d.ok).toBe(true); + if (d.ok) { + expect((d.ctx as OptRequestContext).principal).toBeUndefined(); + expect(d.ctx.tenantId).toBe('tenant_header'); + } + }); + + it('never rejects even when a present token fails verification (logs a warning)', async () => { + const logger = { warn: vi.fn() }; + const d = await resolveAuthContext(h({ authorization: 'Bearer bad.tok.en' }), { + enforce: false, + verify: failVerify, + logger, + }); + expect(d.ok).toBe(true); + if (d.ok) expect(d.ctx.principal).toBeUndefined(); + expect(logger.warn).toHaveBeenCalledOnce(); + }); + + it('attaches the principal when a present token verifies, without overriding a supplied tenant', async () => { + const d = await resolveAuthContext( + h({ authorization: 'Bearer good.tok.en', 'x-tenant-id': 'tenant_header' }), + { enforce: false, verify: okVerify }, + ); + expect(d.ok).toBe(true); + if (d.ok) { + expect(d.ctx.principal).toEqual(principal); + // non-breaking: caller-supplied header tenant is preserved + expect(d.ctx.tenantId).toBe('tenant_header'); + } + }); +}); diff --git a/tests/auth/zitadel.test.ts b/tests/auth/zitadel.test.ts new file mode 100644 index 0000000..e059208 --- /dev/null +++ b/tests/auth/zitadel.test.ts @@ -0,0 +1,124 @@ +/** + * Unit tests for src/auth/zitadel.ts `verifyBearer`, using a locally-generated + * RSA keypair + `createLocalJWKSet` so there is ZERO network access: we sign + * our own tokens and inject the matching JWKS resolver. + */ + +import { describe, it, expect, beforeAll } from 'vitest'; +import { + SignJWT, + generateKeyPair, + exportJWK, + createLocalJWKSet, + type JWTVerifyGetKey, + type KeyLike, +} from 'jose'; +import { verifyBearer, deriveTenantId, BearerVerificationError } from '@/src/auth/zitadel'; + +const ISSUER = 'https://test-issuer.siax.io'; +const KID = 'test-key-1'; + +let privateKey: KeyLike; +let jwks: JWTVerifyGetKey; + +beforeAll(async () => { + const { publicKey, privateKey: pk } = await generateKeyPair('RS256'); + privateKey = pk; + const jwk = await exportJWK(publicKey); + jwk.kid = KID; + jwk.alg = 'RS256'; + jwks = createLocalJWKSet({ keys: [jwk] }); +}); + +interface SignOpts { + issuer?: string; + expirationTime?: string | number; +} + +async function sign(claims: Record, opts: SignOpts = {}): Promise { + return new SignJWT(claims) + .setProtectedHeader({ alg: 'RS256', kid: KID }) + .setIssuedAt() + .setIssuer(opts.issuer ?? ISSUER) + .setExpirationTime(opts.expirationTime ?? '2h') + .sign(privateKey); +} + +describe('deriveTenantId', () => { + it('prefers the Zitadel org id claim', () => { + expect( + deriveTenantId({ 'urn:zitadel:iam:org:id': 'org_1', tenant_id: 't_custom' }), + ).toBe('org_1'); + }); + + it('falls back to the resourceowner id, then tenant_id, then tenant', () => { + expect(deriveTenantId({ 'urn:zitadel:iam:user:resourceowner:id': 'org_ro' })).toBe('org_ro'); + expect(deriveTenantId({ tenant_id: 't_x' })).toBe('t_x'); + expect(deriveTenantId({ tenant: 't_y' })).toBe('t_y'); + }); + + it('honors an explicit override claim key when present', () => { + expect(deriveTenantId({ custom_tid: 't_over', 'urn:zitadel:iam:org:id': 'org_1' }, 'custom_tid')).toBe( + 't_over', + ); + }); + + it('returns undefined when no tenant claim is present', () => { + expect(deriveTenantId({ sub: 'u1' })).toBeUndefined(); + }); +}); + +describe('verifyBearer', () => { + it('verifies a valid token and derives the principal (sub + tenant)', async () => { + const token = await sign({ sub: 'user_1', 'urn:zitadel:iam:org:id': 'tenant_x' }); + const principal = await verifyBearer(token, { issuer: ISSUER, keyResolver: jwks }); + expect(principal.sub).toBe('user_1'); + expect(principal.tenantId).toBe('tenant_x'); + expect(principal.claims.iss).toBe(ISSUER); + }); + + it('honors the custom tenant_id claim', async () => { + const token = await sign({ sub: 'user_2', tenant_id: 't_protosell' }); + const principal = await verifyBearer(token, { issuer: ISSUER, keyResolver: jwks }); + expect(principal.tenantId).toBe('t_protosell'); + }); + + it('rejects a tampered token (bad signature)', async () => { + const token = await sign({ sub: 'user_1', 'urn:zitadel:iam:org:id': 'tenant_x' }); + const firstDot = token.indexOf('.'); + const secondDot = token.indexOf('.', firstDot + 1); + const mid = Math.floor((firstDot + secondDot) / 2); + const ch = token.charAt(mid); + const tampered = token.slice(0, mid) + (ch === 'a' ? 'b' : 'a') + token.slice(mid + 1); + await expect(verifyBearer(tampered, { issuer: ISSUER, keyResolver: jwks })).rejects.toBeInstanceOf( + BearerVerificationError, + ); + }); + + it('rejects an expired token', async () => { + const token = await sign( + { sub: 'user_1', 'urn:zitadel:iam:org:id': 'tenant_x' }, + { expirationTime: '-1h' }, + ); + await expect(verifyBearer(token, { issuer: ISSUER, keyResolver: jwks })).rejects.toBeInstanceOf( + BearerVerificationError, + ); + }); + + it('rejects a token from the wrong issuer', async () => { + const token = await sign( + { sub: 'user_1', 'urn:zitadel:iam:org:id': 'tenant_x' }, + { issuer: 'https://evil-issuer.example' }, + ); + await expect(verifyBearer(token, { issuer: ISSUER, keyResolver: jwks })).rejects.toBeInstanceOf( + BearerVerificationError, + ); + }); + + it('rejects a validly-signed token that carries no tenant claim', async () => { + const token = await sign({ sub: 'user_1' }); + await expect(verifyBearer(token, { issuer: ISSUER, keyResolver: jwks })).rejects.toMatchObject({ + reason: 'token_missing_tenant_claim', + }); + }); +}); diff --git a/tests/validation/0pt-schemas.test.ts b/tests/validation/0pt-schemas.test.ts new file mode 100644 index 0000000..b4e2160 --- /dev/null +++ b/tests/validation/0pt-schemas.test.ts @@ -0,0 +1,142 @@ +/** + * Unit tests for src/validation/0pt-schemas.ts -- the Wave 2 request-body + * validation for 0pt's mutating capability routes. Confirms a valid body + * passes and a malformed body produces a structured 400. + */ + +import { describe, it, expect } from 'vitest'; +import { + parseBody, + documentCaptureSchema, + documentExtractionSchema, + sourceQuerySchema, + createSourceRatePolicySchema, + mapDatasetToIcebergTableSchema, + requestIcebergWriteSchema, +} from '@/src/validation/0pt-schemas'; + +describe('parseBody + documentCaptureSchema', () => { + const valid = { + mailbox: { + tenant_id: 't1', + client_id: 'c1', + client_secret: 's1', + mailbox_address: 'ap@x.io', + }, + tenant_id: 't1', + project_id: 'p1', + }; + + it('accepts a valid capture body', () => { + const r = parseBody(documentCaptureSchema, valid); + expect(r.ok).toBe(true); + }); + + it('rejects a body missing tenant_id with a 400 + issues', () => { + const { tenant_id: _omit, ...bad } = valid; + const r = parseBody(documentCaptureSchema, bad); + expect(r.ok).toBe(false); + if (!r.ok) { + expect(r.status).toBe(400); + expect(r.body.error).toBe('validation_error'); + expect(r.body.issues.some((i) => i.path === 'tenant_id')).toBe(true); + } + }); + + it('rejects a wrong-typed top field', () => { + const r = parseBody(documentCaptureSchema, { ...valid, top: 'ten' }); + expect(r.ok).toBe(false); + }); +}); + +describe('documentExtractionSchema', () => { + it('accepts minimal valid input', () => { + expect(parseBody(documentExtractionSchema, { tenant_id: 't', project_id: 'p' }).ok).toBe(true); + }); + it('rejects empty body', () => { + expect(parseBody(documentExtractionSchema, {}).ok).toBe(false); + }); +}); + +describe('sourceQuerySchema', () => { + it('accepts a valid query', () => { + const r = parseBody(sourceQuerySchema, { + query: 'SELECT 1', + tenant_id: 't', + project_id: 'p', + connection_config: { host: 'db' }, + }); + expect(r.ok).toBe(true); + }); + it('rejects a blank query', () => { + const r = parseBody(sourceQuerySchema, { + query: ' ', + tenant_id: 't', + project_id: 'p', + connection_config: {}, + }); + expect(r.ok).toBe(false); + }); + it('rejects a missing connection_config', () => { + const r = parseBody(sourceQuerySchema, { query: 'SELECT 1', tenant_id: 't', project_id: 'p' }); + expect(r.ok).toBe(false); + }); +}); + +describe('createSourceRatePolicySchema', () => { + const valid = { + source_rate_policy_id: 'rp1', + tenant_id: 't', + project_id: 'p', + source_id: 's', + rate_limit_per_minute: 60, + crawl_budget: 1000, + }; + it('accepts a valid policy', () => { + expect(parseBody(createSourceRatePolicySchema, valid).ok).toBe(true); + }); + it('rejects a bad access enum', () => { + expect(parseBody(createSourceRatePolicySchema, { ...valid, access: 'ftp' }).ok).toBe(false); + }); + it('rejects a non-numeric rate_limit_per_minute', () => { + expect(parseBody(createSourceRatePolicySchema, { ...valid, rate_limit_per_minute: 'fast' }).ok).toBe(false); + }); +}); + +describe('mapDatasetToIcebergTableSchema', () => { + const valid = { + tenant_id: 't', + project_id: 'p', + dataset_id: 'd', + dataset_iceberg_mapping_id: 'm', + namespace: 'ns', + table_name: 'tbl', + storage_uri: 's3://x', + catalog_ref: 'cat', + actor_id: 'a', + correlation_id: 'corr', + }; + it('accepts a valid mapping', () => { + expect(parseBody(mapDatasetToIcebergTableSchema, valid).ok).toBe(true); + }); + it('rejects a mapping missing storage_uri', () => { + const { storage_uri: _omit, ...bad } = valid; + expect(parseBody(mapDatasetToIcebergTableSchema, bad).ok).toBe(false); + }); +}); + +describe('requestIcebergWriteSchema', () => { + const valid = { + actor_id: 'a', + correlation_id: 'corr', + schema_version: 'v1', + manifest_uri: 's3://m', + }; + it('accepts a valid write request', () => { + expect(parseBody(requestIcebergWriteSchema, valid).ok).toBe(true); + }); + it('rejects a write request missing manifest_uri', () => { + const { manifest_uri: _omit, ...bad } = valid; + expect(parseBody(requestIcebergWriteSchema, bad).ok).toBe(false); + }); +}); -- 2.54.0