diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fafa1a5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,32 @@ +# Keep the build context small and make sure no local secret or build artefact is ever baked into an image. +.git +.github +node_modules +.next +out +build +dist +coverage + +# Secrets: .env.example / .env.test.example are documentation and stay out too — the image is configured +# entirely through the environment. +.env +.env.* + +# Local tooling and editor state +.claude +.vscode +.idea +.DS_Store +*.tsbuildinfo +npm-debug.log* + +# Not needed to build or run the image +tests +docs +README.md +CLAUDE.md +components.json +Dockerfile +docker-compose.yml +.dockerignore diff --git a/.env.example b/.env.example index 0e689d8..98ea260 100644 --- a/.env.example +++ b/.env.example @@ -1,32 +1,102 @@ -# ── Required ──────────────────────────────────────────────────────────────── -# Local Postgres (Homebrew): createdb ai_staffing_agency +# ═══════════════════════════════════════════════════════════════════════════════ +# AI Staffing Agency — environment +# +# cp .env.example .env then fill in the three REQUIRED values below. +# +# Every variable here is validated at boot by src/server/env.ts: a bad value stops +# the worker immediately and, in production, stops the web process too. Defaults in +# brackets are what src/server/config.ts uses when the variable is unset. +# Deployment guidance: docs/DEPLOYMENT.md · Security: docs/SECURITY.md +# ═══════════════════════════════════════════════════════════════════════════════ + +# ── REQUIRED ──────────────────────────────────────────────────────────────────── +# PostgreSQL 14+. Local (Homebrew): createdb ai_staffing_agency +# Production: add sslmode=require (or verify-full) and connection_limit — see DEPLOYMENT.md § Pooling. DATABASE_URL="postgresql://YOUR_USER@localhost:5432/ai_staffing_agency?schema=public&options=-c%20TimeZone%3DUTC" -# openssl rand -base64 32 + +# Session signing key. Generate one — an empty value fails boot: openssl rand -base64 32 +# Rotating it signs every user out. Put the old value in AUTH_SECRET_PREVIOUS for a grace period. AUTH_SECRET="" -AUTH_TRUST_HOST=true -# openssl rand -base64 32 (AES-256-GCM key for the credentials vault) + +# AES-256-GCM key for the org credentials vault. Must be base64 that decodes to exactly 32 bytes: +# openssl rand -base64 32 +# WARNING: losing or rotating this key makes every stored customer credential unreadable. Escrow it +# separately from your database backups. See docs/DEPLOYMENT.md § Key rotation. CREDENTIAL_ENCRYPTION_KEY="" -# ── Optional: model providers ─────────────────────────────────────────────── -# With no keys set the app runs in clearly-badged "Simulated mode" using a deterministic mock provider. +# ── URLs and proxies ──────────────────────────────────────────────────────────── +# Public origin. REQUIRED in production and must be https. NEXTAUTH_URL is accepted as an alias. +# AUTH_URL="https://app.example.com" +# Only set when a proxy in front of the app rewrites Host/X-Forwarded-*; AUTH_URL alone is safer. +AUTH_TRUST_HOST=true +# Number of reverse proxies whose X-Forwarded-For entries are trusted for rate limiting [1 in prod, 0 in dev]. +# TRUSTED_PROXY_HOPS=1 +# Extra hosts allowed to post Server Actions (read by next.config.ts) — comma separated, host only. +# SERVER_ACTIONS_ALLOWED_ORIGINS="app.example.com" +# Must be identical across web instances and builds, or Server Action payloads fail to decrypt: +# openssl rand -base64 32 +# NEXT_SERVER_ACTIONS_ENCRYPTION_KEY="" + +# ── Key rotation (optional, both accept the previous value during a rollout) ───── +# AUTH_SECRET_PREVIOUS="" +# CREDENTIAL_ENCRYPTION_KEY_PREVIOUS="" + +# ── Executor / worker ─────────────────────────────────────────────────────────── +# Runs are always executed by the separate worker process (`npm run worker`). +# "inline" only tells `npm run dev` to start a worker alongside the web server; "off" = a dedicated +# `npm run worker` process does (production default). src/worker.ts ignores this and always executes. +# EXECUTOR_MODE=inline +# EXECUTOR_DISABLED=true # deprecated alias for EXECUTOR_MODE=off +# EXECUTOR_POLL_MS=1000 # queue poll interval [1000] +# EXECUTOR_CONCURRENCY=2 # runs in flight per executor process [2] +# EXECUTOR_STALE_LOCK_MS=120000 # a RUNNING run with no heartbeat for this long is recovered [120000] +# SCHEDULER_TICK_MS=30000 # how often due scheduled workers are queued [30000] +# How long a stopping worker waits for in-flight runs before handing their leases back [20000]. +# Keep it BELOW the orchestrator's termination grace period (Docker/K8s default 30 s). +# SHUTDOWN_GRACE_MS=20000 +# SERVICE_NAME=worker # logger field distinguishing web from worker [web] + +# ── Sign-up and demo access ───────────────────────────────────────────────────── +# open = anyone can create a workspace · invite = SIGNUP_INVITE_CODE required · closed = invitations only. +# Defaults to "open" in dev and "closed" in production. +# SIGNUP_MODE=closed +# SIGNUP_INVITE_CODE="" # ≥ 12 chars; required when SIGNUP_MODE=invite +# DEMO_MODE=true # pre-fills the demo credentials on the sign-in page +# Required before `npm run db:seed:demo` will touch a production database. The demo workspace +# ships a publicly documented password — never set this on a deployment with real customers. +# ALLOW_DEMO_SEED=true + +# ── Model providers ───────────────────────────────────────────────────────────── +# With no keys set the app runs in clearly-badged "Simulated mode" on a deterministic mock provider. OPENAI_API_KEY= ANTHROPIC_API_KEY= GOOGLE_GENERATIVE_AI_API_KEY= - # Override which model serves each tier: ":" # MODEL_TIER_FAST=anthropic:claude-haiku-4-5 # MODEL_TIER_STANDARD=anthropic:claude-sonnet-5 # MODEL_TIER_REASONING=anthropic:claude-opus-5 - -# Force the mock provider even when keys exist (useful for demos/tests) +# Force the mock provider even when keys exist (demos, load tests). # FORCE_SIMULATED=true -# ── Optional: tools ───────────────────────────────────────────────────────── -# Real web search (https://tavily.com). Without it, web_search returns realistic simulated results. +# ── Tools ─────────────────────────────────────────────────────────────────────── +# Real web search (https://tavily.com). Without it, web_search returns simulated results. TAVILY_API_KEY= -# ── Optional: runtime ─────────────────────────────────────────────────────── -# EXECUTOR_DISABLED=true # don't start the in-process executor/scheduler -# EXECUTOR_POLL_MS=1000 -# EXECUTOR_CONCURRENCY=2 -# USAGE_MARGIN_MULTIPLIER=1.4 # billable = cost × multiplier +# ── Limits and spend ──────────────────────────────────────────────────────────── +# PLATFORM_DEFAULT_MONTHLY_BUDGET_USD=25 # per org, when Organization.monthlyBudgetUsd is null [25] +# PLATFORM_MAX_COST_PER_RUN_USD=5 # ceiling a blueprint's own limit is clamped to [5] +# PLATFORM_MAX_TOOL_CALLS_PER_RUN=60 # [60] +# PLATFORM_MAX_RUN_DURATION_SEC=1800 # [1800] +# USAGE_MARGIN_MULTIPLIER=1.4 # billable = cost × multiplier [1.4] +# Development escape hatch only — rejected at boot when NODE_ENV=production. +# RATE_LIMIT_DISABLED=true + +# ── Retention (enforced by the worker's maintenance sweep) ────────────────────── +# RETENTION_TRACE_DAYS=30 # clear model prompts/responses and step payloads after N days [30] +# RETENTION_EVENTS_DAYS=365 # delete activity + security events after N days [365] + +# ── Observability ─────────────────────────────────────────────────────────────── +# LOG_LEVEL=info # debug | info | warn | error [info] +# LOG_FORMAT=json # json | pretty [json in production, pretty otherwise] +# APP_VERSION= # git sha baked in at image build; reported by /api/health +# PRISMA_LOG=query # NEVER in production: logs SQL including parameters diff --git a/.env.test.example b/.env.test.example index 936450f..9d8536d 100644 --- a/.env.test.example +++ b/.env.test.example @@ -1,8 +1,19 @@ # Copy to .env.test for `npm test`. Tests always run in Simulated mode against a dedicated database: # createdb ai_staffing_agency_test +# tests/setup/env.ts additionally forces FORCE_SIMULATED, EXECUTOR_MODE=off and RATE_LIMIT_DISABLED, +# and clears every provider key, so a suite can never reach a real API or execute a run in the background. +# Keep this file in sync with .env.example and with .github/workflows/ci.yml. + +# Must contain "_test": tests/setup/global-setup.ts refuses to run migrations against anything else. DATABASE_URL="postgresql://YOUR_USER@localhost:5432/ai_staffing_agency_test?schema=public&options=-c%20TimeZone%3DUTC" -AUTH_SECRET="test-secret-not-used-in-production" + +AUTH_SECRET="test-secret-not-used-in-production-0123456789" AUTH_TRUST_HOST=true -# openssl rand -base64 32 -CREDENTIAL_ENCRYPTION_KEY="" + +# Fixed throwaway key so the credentials-vault suites are reproducible. Test data only — +# generate a real one for any other environment: openssl rand -base64 32 +CREDENTIAL_ENCRYPTION_KEY="dGVzdC1vbmx5LWtleS1kby1ub3QtdXNlLWluLXByb2Q=" + +# Runs are executed by calling executeRun() directly; no background loop in tests. +EXECUTOR_MODE=off EXECUTOR_DISABLED=true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8de97ab --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,121 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + verify: + name: typecheck · lint · test · build + runs-on: ubuntu-latest + timeout-minutes: 30 + + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: ci + POSTGRES_PASSWORD: ci + POSTGRES_DB: ai_staffing_agency_test + TZ: UTC + ports: ["5432:5432"] + options: >- + --health-cmd "pg_isready -U ci" + --health-interval 5s + --health-timeout 5s + --health-retries 12 + + env: + # Must contain "_test": tests/setup/global-setup.ts refuses any other database. + DATABASE_URL: postgresql://ci:ci@localhost:5432/ai_staffing_agency_test?schema=public&options=-c%20TimeZone%3DUTC + SHADOW_DATABASE_URL: postgresql://ci:ci@localhost:5432/ai_staffing_agency_shadow?schema=public + AUTH_SECRET: ci-only-secret-0123456789abcdef0123456789abcdef + AUTH_URL: http://localhost:3000 + AUTH_TRUST_HOST: "true" + # Throwaway 32-byte key; the vault suites need a valid one. Never used outside CI. + CREDENTIAL_ENCRYPTION_KEY: dGVzdC1vbmx5LWtleS1kby1ub3QtdXNlLWluLXByb2Q= + EXECUTOR_MODE: "off" + FORCE_SIMULATED: "true" + NEXT_TELEMETRY_DISABLED: "1" + TZ: UTC + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + # `npm ci` runs `prisma generate` via postinstall. + - run: npm ci + + - name: Apply migrations to the test database + run: npx prisma migrate deploy + + - name: Fail if schema.prisma has drifted from prisma/migrations + run: | + PGPASSWORD=ci createdb -h localhost -U ci ai_staffing_agency_shadow + npx prisma migrate diff \ + --from-migrations prisma/migrations \ + --to-schema-datamodel prisma/schema.prisma \ + --shadow-database-url "$SHADOW_DATABASE_URL" \ + --exit-code + + - name: Fail if the migration manifest is stale + run: | + npm run migrations:manifest + git diff --exit-code -- src/generated/migrations.json + + - run: npx tsc --noEmit -p . + + - run: npx eslint . + + - run: npx vitest run + + - name: Cache the Next.js build + uses: actions/cache@v4 + with: + path: .next/cache + key: next-${{ hashFiles('package-lock.json') }}-${{ hashFiles('src/**/*.[jt]s', 'src/**/*.[jt]sx') }} + restore-keys: | + next-${{ hashFiles('package-lock.json') }}- + + - name: Build the web app + run: npm run build + env: + NODE_ENV: production + APP_VERSION: ${{ github.sha }} + + - name: Build the worker bundle + run: npm run build:worker + + - name: Audit production dependencies + run: npm run audit:prod + + image: + name: docker image + needs: verify + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + # Build only: publishing is deliberately left to the deploy workflow that owns the registry credentials. + - uses: docker/build-push-action@v6 + with: + context: . + target: runner + push: false + build-args: APP_VERSION=${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 801b5be..68550fd 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ next-env.d.ts # local tooling .claude/ + +# worker bundle (npm run build:worker) +/dist diff --git a/CLAUDE.md b/CLAUDE.md index 8ce0a44..63480b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ **Product:** the staffing agency for AI workers. A company describes a job in plain English → the platform scopes it (JobSpec) → designs an AI worker (WorkerBlueprint) → the user *hires* it → it executes runs → produces deliverables → is evaluated → can be talked to, improved, or *replaced* like a contractor. Core loop: **Job → Worker → Runs → Deliverables → Evaluation → Replace.** -**Read `docs/CONTRACTS.md` before writing any code.** It defines module boundaries, public function signatures, and file ownership. The typed contracts live in code: `prisma/schema.prisma`, `src/server/domain/*`, `src/server/*/types.ts`. +**Read `docs/CONTRACTS.md` before writing any code.** For the production/security/redesign phase also read `docs/PRODUCTION.md` (contracts + file ownership that override the FROZEN list for assigned files) and `docs/DESIGN.md` (the design system every UI change follows). It defines module boundaries, public function signatures, and file ownership. The typed contracts live in code: `prisma/schema.prisma`, `src/server/domain/*`, `src/server/*/types.ts`. ## Stack (pinned — do not upgrade or swap) Next.js 15 App Router · React 19 · strict TypeScript · Tailwind v4 · shadcn/ui (radix, components already in `src/components/ui`) · Prisma 6 + PostgreSQL 14 · Auth.js v5 (credentials) · Vercel AI SDK **v5** (`ai@5`, `@ai-sdk/*@2`) · Zod **v4** · Vitest · lucide-react · recharts · date-fns · sonner. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3d235c6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,73 @@ +# syntax=docker/dockerfile:1.7 +# +# One image, three roles (docs/DEPLOYMENT.md): +# web → node server.js (default CMD; Next.js standalone server) +# worker → node dist/worker.cjs (the run executor) +# migrate → npx prisma migrate deploy (one-shot release job, run before rolling web/worker) +# +# Debian slim rather than Alpine: Prisma's query engine ships a glibc/OpenSSL 3 binary, so this avoids adding a +# musl binaryTarget to the frozen schema. + +# ── base ──────────────────────────────────────────────────────────────────────── +FROM node:22-bookworm-slim AS base +ENV NEXT_TELEMETRY_DISABLED=1 +RUN apt-get update \ + && apt-get install -y --no-install-recommends openssl ca-certificates \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /app + +# ── deps: full dependency tree for building ───────────────────────────────────── +FROM base AS deps +COPY package.json package-lock.json ./ +COPY prisma ./prisma +RUN --mount=type=cache,target=/root/.npm npm ci + +# ── build: next build + the worker bundle ─────────────────────────────────────── +FROM deps AS build +ARG APP_VERSION=dev +ENV APP_VERSION=$APP_VERSION +COPY . . +# The migration manifest (npm prebuild) is what /api/ready compares against the database. +RUN npx prisma generate \ + && npm run build \ + && npm run build:worker + +# ── prod-deps: runtime dependency tree (no devDependencies) ───────────────────── +# The worker bundle and the prisma CLI both resolve from here; the standalone server overlays its traced copies. +FROM base AS prod-deps +COPY package.json package-lock.json ./ +COPY prisma ./prisma +RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev \ + && npx prisma generate + +# ── runner ────────────────────────────────────────────────────────────────────── +FROM base AS runner +ARG APP_VERSION=dev +ENV NODE_ENV=production \ + APP_VERSION=$APP_VERSION \ + PORT=3000 \ + HOSTNAME=0.0.0.0 \ + TZ=UTC \ + SERVICE_NAME=web \ + EXECUTOR_MODE=off + +COPY --from=prod-deps --chown=node:node /app/node_modules ./node_modules +COPY --from=build --chown=node:node /app/.next/standalone ./ +COPY --from=build --chown=node:node /app/.next/static ./.next/static +COPY --from=build --chown=node:node /app/public ./public +COPY --from=build --chown=node:node /app/dist ./dist +# schema + migrations for the release job +COPY --from=build --chown=node:node /app/prisma ./prisma + +# Next writes its ISR/image cache here. +RUN mkdir -p .next/cache && chown -R node:node .next + +USER node +EXPOSE 3000 +STOPSIGNAL SIGTERM + +# Liveness only: /api/health touches no database, so a probe never queues behind a slow query. +HEALTHCHECK --interval=15s --timeout=3s --start-period=20s --retries=3 \ + CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + +CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f228423 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,92 @@ +# Local/staging stack: Postgres, a one-shot migration job, the web tier and the run worker. +# +# cp .env.example .env # fill in AUTH_SECRET and CREDENTIAL_ENCRYPTION_KEY +# docker compose up --build +# +# Secrets are never baked into an image: every service reads .env at run time. DATABASE_URL is overridden here +# because the database is reachable as "db" on the compose network, not on localhost. + +name: ai-staffing-agency + +x-app: &app + build: + context: . + target: runner + args: + APP_VERSION: ${APP_VERSION:-compose} + env_file: .env + environment: + NODE_ENV: production + TZ: UTC + DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-app}@db:5432/${POSTGRES_DB:-ai_staffing_agency}?schema=public&connection_limit=10&pool_timeout=20 + depends_on: + db: + condition: service_healthy + +services: + db: + image: postgres:16 + environment: + POSTGRES_USER: ${POSTGRES_USER:-app} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-app} + POSTGRES_DB: ${POSTGRES_DB:-ai_staffing_agency} + TZ: UTC + PGTZ: UTC + # The app stores UTC and several queries compare against now(); pin the server clock to UTC. + command: ["postgres", "-c", "timezone=UTC", "-c", "log_min_duration_statement=1000"] + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-app} -d ${POSTGRES_DB:-ai_staffing_agency}"] + interval: 5s + timeout: 3s + retries: 12 + ports: + - "${POSTGRES_PORT:-5433}:5432" + + # Release step. Runs to completion before web and worker start, so the schema is never behind the code. + migrate: + <<: *app + command: ["npx", "prisma", "migrate", "deploy"] + restart: "no" + + web: + <<: *app + environment: + NODE_ENV: production + TZ: UTC + SERVICE_NAME: web + EXECUTOR_MODE: "off" + DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-app}@db:5432/${POSTGRES_DB:-ai_staffing_agency}?schema=public&connection_limit=10&pool_timeout=20 + command: ["node", "server.js"] + ports: + - "${WEB_PORT:-3000}:3000" + depends_on: + db: + condition: service_healthy + migrate: + condition: service_completed_successfully + restart: unless-stopped + + worker: + <<: *app + environment: + NODE_ENV: production + TZ: UTC + SERVICE_NAME: worker + # Below stop_grace_period, so leases are handed back before Docker sends SIGKILL. + SHUTDOWN_GRACE_MS: "20000" + DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-app}@db:5432/${POSTGRES_DB:-ai_staffing_agency}?schema=public&connection_limit=10&pool_timeout=20 + command: ["node", "dist/worker.cjs"] + healthcheck: + disable: true + stop_grace_period: 30s + depends_on: + db: + condition: service_healthy + migrate: + condition: service_completed_successfully + restart: unless-stopped + +volumes: + pgdata: diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..bc512a8 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,353 @@ +# Deployment + +How to run the AI Staffing Agency in production: what the pieces are, how to configure them, how to release, and +what to do when something breaks. Threat model, key handling and incident response live in +[`docs/SECURITY.md`](./SECURITY.md); day-2 conventions live in [`docs/OPERATIONS.md`](./OPERATIONS.md). + +--- + +## 1. Architecture + +Three processes and one database. Nothing is held in memory between requests — every decision, lease and +checkpoint is a row — so each tier scales on its own. + +``` + ┌──────────────┐ ┌────────────────────────────┐ + users ──▶ │ web (N) │ ─────▶ │ │ + │ next start │ │ PostgreSQL 14+ │ + │ EXECUTOR_ │ │ (managed, PITR on) │ + │ MODE=off │ │ │ + └──────────────┘ │ Run queue · leases · │ + │ checkpoints · usage │ + ┌──────────────┐ │ │ + │ worker (N) │ ─────▶ │ │ + │ dist/ │ └────────────────────────────┘ + │ worker.cjs │ + └──────────────┘ + │ + └─▶ model providers (or the deterministic mock in Simulated mode), tools +``` + +**web** — the Next.js app (`output: "standalone"`). Stateless; run as many replicas as you like behind a load +balancer. It never executes runs in production: `EXECUTOR_MODE=off` keeps `src/instrumentation.ts` from starting +the in-process executor, so scaling the web tier does not silently scale run concurrency. + +**worker** — `src/worker.ts`, bundled to `dist/worker.cjs`. Claims queued runs, drives them through the +blueprint, recovers crashed leases, ticks the scheduler and runs the maintenance sweeps (retention, rate-limit +buckets, stale heartbeats). It always executes; `EXECUTOR_MODE` only governs the web process. + +**database** — PostgreSQL 14 or later. It is the queue, the lease manager and the ledger. There is no Redis and +no message broker to operate. + +Local development runs the same two processes: `npm run dev` starts the Next.js server and a worker side by side (`scripts/dev.mjs`). The web process never executes runs in any environment — it only enqueues them. + +--- + +## 2. Environment reference + +Every variable is validated at boot by `src/server/env.ts`. A bad value stops the worker immediately (exit code +78, `EX_CONFIG`) and, in production, stops the web process too — a misconfigured deploy fails fast instead of +failing on the first request that needs the missing value. `.env.example` is the annotated master copy. + +### Required everywhere + +| Variable | Notes | +|---|---| +| `DATABASE_URL` | `postgres://…`. Add `sslmode=require` and `connection_limit` in production (§5). | +| `AUTH_SECRET` | `openssl rand -base64 32`. ≥ 32 chars in production. Rotating it signs everyone out. | +| `CREDENTIAL_ENCRYPTION_KEY` | base64 of exactly 32 bytes: `openssl rand -base64 32`. Encrypts the org credentials vault. | + +### Required in production + +| Variable | Notes | +|---|---| +| `NODE_ENV=production` | Turns on JSON logging, https cookie rules, the closed-signup default and the demo-seed guard. | +| `AUTH_URL` | The public origin, `https://app.example.com`. Pins Auth.js callbacks instead of trusting `X-Forwarded-Host`. | +| `APP_VERSION` | Git sha baked in at image build; reported by `/api/health` and on every log line's deploy. | + +`RATE_LIMIT_DISABLED` is **rejected** in production, as is a `DATABASE_URL` containing `sslmode=disable`. + +### Commonly set + +| Variable | Default | Notes | +|---|---|---| +| `EXECUTOR_MODE` | `off` in prod, `inline` in dev | `inline` makes `npm run dev` start a worker alongside the web server; `off` starts the web server alone. The deployed web process never executes runs either way. | +| `EXECUTOR_CONCURRENCY` | `2` | Runs in flight per worker process. Total parallelism = replicas × this. | +| `EXECUTOR_POLL_MS` | `1000` | Queue poll interval. | +| `EXECUTOR_STALE_LOCK_MS` | `120000` | A `RUNNING` run with no heartbeat for this long is recovered (costs one attempt). | +| `SHUTDOWN_GRACE_MS` | `20000` | Drain window on SIGTERM. **Must be below** the orchestrator's termination grace period. | +| `SIGNUP_MODE` | `closed` in prod | `open` \| `invite` \| `closed`. | +| `SIGNUP_INVITE_CODE` | — | ≥ 12 chars; required when `SIGNUP_MODE=invite`. | +| `TRUSTED_PROXY_HOPS` | `1` in prod | Number of proxies whose `X-Forwarded-For` entries you trust for rate limiting. | +| `LOG_LEVEL` / `LOG_FORMAT` | `info` / `json` in prod | | +| `SERVICE_NAME` | `web` | Set to `worker` on the worker so one log stream is filterable. | +| `RETENTION_TRACE_DAYS` | `30` | Model prompts/responses and step payloads are cleared after this (§9). | +| `RETENTION_EVENTS_DAYS` | `365` | Activity + security events are deleted after this. | +| `PLATFORM_*` | see `.env.example` | Per-run and per-org spend ceilings. | +| `ALLOW_DEMO_SEED` | unset | Required before the demo seed will touch a production database. See §10. | +| `FORCE_SIMULATED` | unset | Forces the deterministic mock provider. With no provider key set, Simulated mode is the default. | + +Provider keys (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY`, `TAVILY_API_KEY`) are +optional. **Without them the product runs fully, in clearly badged Simulated mode.** That is the right default +for a demo and the wrong default for paying customers: if you intend real model calls, confirm the badge is gone +after the first deploy. + +--- + +## 3. First deploy + +1. **Provision Postgres.** Create the database and pin its clock (§5.2). Turn on point-in-time recovery. +2. **Generate secrets.** `openssl rand -base64 32` twice, for `AUTH_SECRET` and `CREDENTIAL_ENCRYPTION_KEY`. + Store them in your platform's secret manager. Escrow the encryption key **separately from database backups**: + whoever holds both holds the customer credentials. +3. **Build the image.** `docker build --build-arg APP_VERSION=$(git rev-parse --short HEAD) .` +4. **Run migrations** as a one-shot job, before any new code starts: + `npx prisma migrate deploy` (in the image: `docker run --rm --env-file .env IMAGE npx prisma migrate deploy`). +5. **Start web.** Wait for `/api/ready` to return 200 before putting it in the load balancer rotation. +6. **Start the worker.** It is idle until the first run is queued. +7. **Create the first account.** There is no bootstrap admin on purpose. Either: + - set `SIGNUP_MODE=open` (or `invite` with a `SIGNUP_INVITE_CODE`), sign up at `/sign-up`, then set + `SIGNUP_MODE=closed` and redeploy; or + - seed nothing and have an existing owner invite colleagues from **Settings → Members** (invite links are + shown to the inviting admin to share — there is no email provider in Phase 1). +8. **Verify.** `/api/health` → 200, `/api/ready` → 200 with `checks.migrations: "ok"`, hire a worker, run it once, + and check the run reaches `SUCCEEDED`. + +### Release order for every subsequent deploy + +``` +build image → migrate job (must succeed) → roll worker → roll web +``` + +Migrations first because both tiers assume the schema is at least as new as their code. Workers before web so a +run started by the old code is finished by a process that understands it. Never put `migrate deploy` in a +container's `CMD`: N replicas would race, and Prisma's advisory lock turns that into N failed containers. + +--- + +## 4. Containers + +`Dockerfile` builds one image with three roles: + +| Role | Command | Notes | +|---|---|---| +| web | `node server.js` (default `CMD`) | Next.js standalone server on `$PORT` (3000). | +| worker | `node dist/worker.cjs` | `STOPSIGNAL SIGTERM`; give it ≥ 30 s to stop. | +| migrate | `npx prisma migrate deploy` | One-shot release job. | + +It runs as the non-root `node` user and carries a `HEALTHCHECK` against `/api/health`. `.dockerignore` keeps +`.env*`, `.git`, `node_modules` and test fixtures out of the build context, so nothing secret is ever baked in — +the image is configured entirely through the environment. + +`docker-compose.yml` brings the whole stack up locally or on a single box: Postgres with a health check, the +`migrate` one-shot (web and worker wait for `service_completed_successfully`), web, and worker. + +```bash +cp .env.example .env # fill in AUTH_SECRET and CREDENTIAL_ENCRYPTION_KEY +docker compose up --build +``` + +### Vercel for web, worker elsewhere + +The web tier deploys to Vercel unchanged. Two things to get right: + +- Set `EXECUTOR_MODE=off`. Serverless functions freeze between requests, so runs must be executed by a worker deployed elsewhere + mid-flight — and it is the production default anyway. +- Vercel cannot host the worker: it is a long-lived process. Run `dist/worker.cjs` on Fly.io, Render, Railway, + ECS or any box with `systemd`, pointed at the same `DATABASE_URL`. Give it a SIGTERM grace period above + `SHUTDOWN_GRACE_MS`, and set `SERVICE_NAME=worker`. +- Run `prisma migrate deploy` from a release command / pre-deploy hook, not from the build step. +- Set `SERVER_ACTIONS_ALLOWED_ORIGINS` to your public host, and set the same + `NEXT_SERVER_ACTIONS_ENCRYPTION_KEY` on every instance — separately built instances otherwise fail to decrypt + each other's Server Action payloads. + +--- + +## 5. Database + +### 5.1 Connection pooling + +Each process opens a Prisma pool of `num_cpus × 2 + 1` connections unless you say otherwise, which is how a +modest fleet exhausts the default `max_connections = 100`. + +- web: `connection_limit=5` … `10` per replica. +- worker: `connection_limit` ≥ `EXECUTOR_CONCURRENCY × 2 + 2` — a run holds its heartbeat, its slice writes and + its usage increments at the same time. +- Keep `Σ (replicas × connection_limit)` under ~80 % of `max_connections`. + +Example: `postgresql://app:…@host:5432/app?schema=public&connection_limit=10&pool_timeout=20&sslmode=require` + +With PgBouncer in **transaction** mode, add `pgbouncer=true` to the pooled URL and give +`prisma migrate deploy` a direct URL instead. Interactive transactions and `SELECT … FOR UPDATE` work in +transaction mode; do not introduce session-level advisory locks, which do not. + +### 5.2 Pin the clock to UTC + +The app stores UTC and several queries compare stored timestamps against `now()`. `DATABASE_URL` carries +`options=-c%20TimeZone%3DUTC`, but PgBouncer in transaction mode does not forward startup parameters — it either +rejects the connection or, with `ignore_startup_parameters=options`, silently drops the pin. Set it on the +database instead, once: + +```sql +ALTER DATABASE ai_staffing_agency SET timezone TO 'UTC'; +``` + +Also set `TZ=UTC` in the containers: worker cadence hours ("every weekday at 09:00") are evaluated in the +process's local timezone, so a host on another zone runs scheduled workers at the wrong hour. + +### 5.3 TLS and least privilege + +Require TLS: `sslmode=require`, or `sslmode=verify-full` with `sslrootcert=` pointed at your provider's CA — +`verify-full` is the only setting that actually prevents a man-in-the-middle. + +Use two roles. `app_migrator` owns the schema and is used only by the migrate job. `app_runtime` is what web and +worker connect with and has no DDL rights: + +```sql +GRANT CONNECT ON DATABASE ai_staffing_agency TO app_runtime; +GRANT USAGE ON SCHEMA public TO app_runtime; +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_runtime; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_runtime; +ALTER DEFAULT PRIVILEGES FOR ROLE app_migrator IN SCHEMA public + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_runtime; +``` + +Add guardrails to the runtime URL's `options`: `-c statement_timeout=15000 -c idle_in_transaction_session_timeout=30000`. +Never set `PRISMA_LOG=query` in production — it logs SQL with parameters. + +### 5.4 Backups and restore + +- Managed Postgres with PITR (7–30 days) plus a daily encrypted snapshot in a different account or region. +- **Rehearse the restore quarterly.** A backup you have never restored is a hypothesis. +- Restore drill: provision from the snapshot, point a scratch deploy at it, run `prisma migrate deploy` + (it is idempotent), and check `/api/ready`. +- `CREDENTIAL_ENCRYPTION_KEY` is not in the database. Restoring a backup without it gives you every table and no + readable customer credential. Store it separately and confirm it is in your escrow before you need it. + +### 5.5 Zero-downtime migrations + +Old and new code overlap during a rollout, so every migration must be readable by both. Expand, then contract: + +1. Release A: add nullable columns / new tables / new indexes. Deploy code that writes both shapes. +2. Backfill in batches. +3. Release B: start reading the new shape. +4. Release C: drop the old column, add the `NOT NULL`. + +Index a large table with `CREATE INDEX CONCURRENTLY` in a hand-written migration whose file contains only that +statement (Prisma wraps everything else in a transaction, and `CONCURRENTLY` cannot run inside one). Migrations +are forward-only: roll back by deploying the previous image, not by reversing the schema. + +CI fails if `prisma/schema.prisma` has drifted from `prisma/migrations`, and if `src/generated/migrations.json` +is stale — that manifest is what `/api/ready` compares against the database. + +--- + +## 6. Health checks + +| Endpoint | Probe | Checks | +|---|---|---| +| `GET /api/health` | liveness | Process only — no database. Returns `{status, version, uptimeS}`. | +| `GET /api/ready` | readiness | `SELECT 1` within 2 s; `_prisma_migrations` has no unfinished or rolled-back rows; every migration this build expects is applied. 503 otherwise. | + +Both are unauthenticated and `no-store`. `/api/ready` names *which* check failed but never why — the reason is in +the logs. It also reports executor liveness from `ExecutorHeartbeat` as information; it never fails on it, +because the web tier can serve perfectly well while no worker is running. + +Suggested settings: liveness every 15 s with 3 failures before a restart; readiness every 5 s, and only +`/api/ready` gates load-balancer rotation. + +The worker has no HTTP surface. Watch it through its `ExecutorHeartbeat` row (`seenAt` within ~5 minutes) and +through queue depth. + +--- + +## 7. Scaling + +**Multiple workers are safe by construction.** `claimNextRun` is a guarded `updateMany` on `status = QUEUED`, so +exactly one executor wins a row. Every subsequent write to that run is fenced on +`{ id, status: RUNNING, lockedBy }`, so an executor that has lost its lease cannot write anything. Stale-lease +recovery guards on the exact `heartbeatAt` it read, and the scheduler on the exact `nextRunAt` it read. + +- Max parallel runs = `worker replicas × EXECUTOR_CONCURRENCY`. Raise replicas before concurrency: it gives you + failure isolation as well as throughput. +- Scale the web tier for traffic; it has nothing to do with run capacity. +- **Keep the hosts' clocks in sync (NTP).** Lease staleness compares one host's `new Date()` against another + host's `heartbeatAt`. Skew beyond ~30 s can re-queue a live run. The run is fenced, so nothing is corrupted, + but it costs an attempt. +- `SHUTDOWN_GRACE_MS` below the platform's termination grace period. On SIGTERM the worker stops claiming, waits + out the grace period, and hands every remaining lease back to the queue **without consuming a retry attempt**. + A SIGKILL instead means waiting `EXECUTOR_STALE_LOCK_MS` for recovery *and* burning one of the run's two + attempts — two deploys in a row would fail the run. + +--- + +## 8. Observability + +Logs are one JSON object per line on stdout (`src/server/log`), which is what a log shipper wants. Every record +carries `time`, `level`, `msg` and its bindings — `service`, `component`, `executorId`, `runId`, `orgId`. +Secrets are scrubbed on the way out and stack traces are omitted in production. + +```json +{"time":"2026-09-22T19:35:09.9Z","level":"info","msg":"run.claimed","service":"worker","component":"executor","executorId":"host-1-g1-a1b2c3","runId":"run_x"} +``` + +`instrumentation.ts` logs every request error with its `digest` — the same digest the error page shows the user, +which is how you turn a user's screenshot into a log line. + +Alert on: + +| Signal | Query | +|---|---| +| Readiness failing | `/api/ready` ≠ 200 on any replica for > 1 min | +| No live executor | `max(seenAt)` in `ExecutorHeartbeat` older than 5 min while runs are `QUEUED` | +| Queue backing up | `count(Run where status='QUEUED' and availableAt < now() - 5 min)` | +| Run failure rate | `FAILED / (SUCCEEDED + FAILED)` over 1 h, per org | +| Spend | `OrgSpendMonth` approaching `Organization.monthlyBudgetUsd` | + +--- + +## 9. Data retention + +The worker sweeps at most every 10 minutes (idempotent, so several workers sweeping is harmless): + +| Data | Policy | +|---|---| +| `ModelCall.request` / `.response` | Cleared after `RETENTION_TRACE_DAYS` (30). Tokens, cost, latency are kept. | +| `RunStep.input` / `.output` (finished runs) | Cleared after `RETENTION_TRACE_DAYS`. Titles and timings are kept, so the run timeline still renders. | +| `ActivityEvent`, `SecurityEvent` | Deleted after `RETENTION_EVENTS_DAYS` (365). | +| `RateLimitBucket` | Deleted once idle for 2 days. | +| `ExecutorHeartbeat` | Deleted once stale for 5 minutes. | +| `UsageRecord` | **Never deleted** — it is the billing ledger. | + +Prompts and tool outputs contain whatever a customer asked a worker to read, which is why they are the part that +expires. Everything is batched (1,000 rows per statement) so a sweep never holds locks long enough to block a +run. Deleting an organization cascades to all of its tenant rows. + +--- + +## 10. Demo deployments + +`npm run db:seed:demo` rebuilds the Acme Robotics workspace, including an account whose password is published in +the README. It **refuses to run** when `NODE_ENV=production` unless `ALLOW_DEMO_SEED=true`, and the workspace it +creates is flagged `isDemo` so the UI can badge it. + +`npm run setup` is migrations only — the seed is deliberately not on any release path. Use `npm run setup:dev` +locally when you want both. + +If you do run a public demo: understand that the password is known, keep it in its own database, and never set +`ALLOW_DEMO_SEED` on a deployment that has real customers. + +--- + +## 11. Key rotation + +Full procedures are in [`docs/SECURITY.md`](./SECURITY.md). In short: + +- **`AUTH_SECRET`** — put the old value in `AUTH_SECRET_PREVIOUS`, deploy, wait out the session lifetime, then + drop the previous value. Rotating without the overlap signs every user out. +- **`CREDENTIAL_ENCRYPTION_KEY`** — put the old value in `CREDENTIAL_ENCRYPTION_KEY_PREVIOUS` so existing rows + stay readable while new writes use the new key. Rotating *without* the previous value makes every stored + customer credential permanently unreadable; they would have to be re-entered by hand. +- Provider API keys — swap the environment variable and redeploy. Nothing is cached across processes. + +Rotate immediately if a value ever reaches a log, a ticket or a laptop that is no longer trusted. diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..f422904 --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,826 @@ +# Design system — "Quiet confidence" + +The product UI follows a premium consumer-tech design *language* (large confident type, generous whitespace, one accent colour, frosted chrome, soft depth) under **our own brand**. We borrow a sensibility, not assets: no third-party logos, product names, imagery, copy, or font files. + +This document is the source of truth for every page. `src/components/README.md` documents the component APIs that implement it. + +## Principles + +Design language: "Quiet confidence". The goal is the calm, product-first feel of a premium consumer-tech website, applied to a B2B staffing portal, under our own brand. The current UI is a slate/indigo "Linear clone": 14px text, rings on every card, uppercase eyebrows, icon on every label, and three badges per worker. Those are the tells this redesign removes. + +1. Typography does the work. Hierarchy comes from size and weight, not from boxes, icons or color. Every page has one dominant element: a 32–80px semibold title with tight tracking. Everything else steps down clearly. Never put two things of equal weight side by side. +2. One accent, used rarely. Blue (#0071e3) is for the primary action, links and focus. Nothing else is blue. There is no purple or indigo anywhere. Status colors (green, orange, red) appear only as small dots or text, never as big tinted fills. The exception is the one "needs you" callout. +3. Depth instead of borders. Content sits on white cards with an 18px radius, placed on a #f5f5f7 canvas and lifted by a barely visible shadow. Hairlines (1px, #e5e5ea) are only for separating rows inside a list. No card gets both a ring and a shadow. Never nest a bordered box inside a bordered box. +4. Generous whitespace is the layout. The app has 56px between page sections and 24px of card padding. Marketing sections have 120–160px of vertical padding. When in doubt, remove a divider and add space. +5. Pills for actions, text for navigation. Buttons are rounded-full pills with at most one primary per view. Secondary actions are gray pills. Tertiary actions are blue text links with a trailing chevron ("View all ›"). Navigation is plain text, not icon plus label. +6. Say it in one sentence. Every row, card and empty state leads with a human sentence ("Alex drafted the weekly competitor digest"), with metadata demoted to 13px secondary text on the right. No label:value soup and no ids unless they are needed. +7. Show one status, not three. Each object shows the single most important state (running, needs review, paused), as a dot plus word. Health and score merge into one number. The "Simulated" marker appears once globally in the nav, plus inline on artifacts that were generated. It is not repeated on every section. +8. Frosted chrome, solid content. Only the global nav, local nav, sticky action bars and menus are translucent (backdrop blur 20px, saturate 180%). Page content is never translucent. +9. Motion explains, never decorates. Use 200–400ms ease-out fades and 8–16px slide-ups for things entering the viewport or opening. No bouncing, no parallax, no shimmering gradients, no infinite animations except a live "running" dot. Honor prefers-reduced-motion everywhere. +10. Real UI instead of illustration. Marketing visuals are real HTML/CSS mock cards built from the product's own components. There are no stock images, 3D blobs, gradient orbs, emoji or AI-art. +11. Mobile is a first-class layout, not a squashed desktop. Keep 16px side gutters, 44px minimum touch targets, and 16px minimum input text (so iOS doesn't zoom). The nav collapses to a full-screen frosted menu, and sticky bottom action bars replace right-aligned header buttons. +12. The brand is original. The wordmark is "AI Staffing Agency" with its own three-node glyph in monochrome. No fruit logos, no product names or imagery from any other company, and no borrowed marketing copy. We borrow a sensibility, not assets. + +## Tokens (`src/app/globals.css`) + +```css +/* src/app/globals.css — replace the current :root and @theme inline blocks. Keep the three @imports and the `@custom-variant dark` line (light-only stays). Every shadcn token name is preserved, so the primitives restyle automatically. New tokens follow. */ + +@theme inline { + /* Fonts: the system stack serves SF on Apple devices. Inter (next/font, --font-inter) is the non-Apple fallback. No SF font files ship. */ + --font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", var(--font-inter), "Helvetica Neue", Helvetica, Arial, sans-serif; + --font-display: -apple-system, BlinkMacSystemFont, "SF Pro Display", var(--font-inter), "Helvetica Neue", Helvetica, Arial, sans-serif; + --font-heading: var(--font-display); + --font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; + + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-primary-hover: var(--primary-hover); + --color-primary-active: var(--primary-active); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary-hover: var(--secondary-hover); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-hairline: var(--hairline); + --color-input: var(--input); + --color-ring: var(--ring); + --color-link: var(--link); + --color-tertiary: var(--text-tertiary); + --color-canvas: var(--surface-secondary); + --color-canvas-raised: var(--surface-tertiary); + --color-ink: var(--surface-ink); + --color-ink-foreground: var(--surface-ink-foreground); + --color-success: var(--success); + --color-success-soft: var(--success-soft); + --color-warning: var(--warning); + --color-warning-soft: var(--warning-soft); + --color-danger: var(--danger); + --color-danger-soft: var(--danger-soft); + --color-info: var(--info); + --color-info-soft: var(--info-soft); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + /* Radii are explicit, not multiplier-derived, so each maps to a deliberate role. */ + --radius-xs: 6px; /* checkbox, kbd */ + --radius-sm: 8px; /* menu items, small chips, tooltips */ + --radius-md: 10px; /* segmented-control thumb, code inline */ + --radius-lg: 12px; /* inputs, inset panels, code blocks */ + --radius-xl: 18px; /* cards: the default surface */ + --radius-2xl: 22px; /* dialogs, popovers of substance */ + --radius-3xl: 28px; /* marketing tiles, showcase frames */ + --radius-4xl: 36px; /* hero showcase frame */ + + --shadow-card: var(--elev-1); + --shadow-card-hover: var(--elev-2); + --shadow-popover: var(--elev-3); + --shadow-bar: var(--elev-bar); + + --ease-out: var(--motion-ease-out); + --ease-standard: var(--motion-ease-standard); + --ease-in-out: var(--motion-ease-in-out); + + --container-app: 1200px; + --container-marketing: 1024px; + --container-text: 692px; + --container-reading: 720px; +} + +:root { + /* Surfaces */ + --background: #ffffff; /* default page (marketing, auth, reading view) */ + --foreground: #1d1d1f; /* primary text (16.1:1 on white) */ + --surface-secondary: #f5f5f7; /* app canvas + alternating marketing sections */ + --surface-tertiary: #fbfbfd; /* nav base tint, subtle raised areas */ + --surface-ink: #000000; /* the single dark showcase band on marketing */ + --surface-ink-foreground: #f5f5f7; + --card: #ffffff; + --card-foreground: #1d1d1f; + --popover: #ffffff; /* solid fallback; frosted via .material-thick */ + --popover-foreground: #1d1d1f; + + /* Text roles */ + --muted-foreground: #6e6e73; /* secondary text: 5.0:1 on white, 4.6:1 on #f5f5f7 */ + --text-tertiary: #86868b; /* ONLY for ≥18px text, placeholders, disabled, or decorative numerals (3.6:1) */ + --link: #0066cc; /* inline text links: 5.6:1 */ + + /* Accent: the only brand color */ + --primary: #0071e3; /* 4.6:1 with white text */ + --primary-foreground: #ffffff; + --primary-hover: #0077ed; + --primary-active: #006edb; + --ring: #0071e3; + + /* Neutrals for fills */ + --secondary: #e8e8ed; /* gray pill button, segmented track */ + --secondary-foreground: #1d1d1f; + --secondary-hover: #dcdce1; + --muted: #f5f5f7; /* inset panels, table hover, skeleton base */ + --accent: #f0f0f3; /* menu item hover / highlighted row */ + --accent-foreground: #1d1d1f; + + /* Lines */ + --border: #e5e5ea; /* row dividers, separators */ + --hairline: rgb(0 0 0 / 0.08); /* frosted-bar bottom edge, card edge on white */ + --input: #d2d2d7; /* input borders */ + + /* Status: text-safe foregrounds + whisper-soft fills */ + --success: #248a3d; --success-soft: #eaf6ec; + --warning: #b25000; --warning-soft: #fff4e5; /* "waiting on you" */ + --danger: #d70015; --danger-soft: #fff0f1; + --info: #0071e3; --info-soft: #eaf3fd; /* running / in progress */ + --neutral: #6e6e73; + --destructive: #d70015; + + /* Charts: accent first, then status hues; neutral gray for "other". */ + --chart-1: #0071e3; + --chart-2: #6e6e73; + --chart-3: #248a3d; + --chart-4: #c75d00; + --chart-5: #d70015; + --chart-grid: #ececf0; + --chart-axis: #86868b; + + /* Radius base (kept for shadcn internals that reference --radius) */ + --radius: 12px; + + /* "sidebar" tokens now style the global nav, mobile menu and Sheet. */ + --sidebar: rgb(251 251 253 / 0.8); + --sidebar-foreground: #1d1d1f; + --sidebar-primary: #0071e3; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: rgb(0 0 0 / 0.05); + --sidebar-accent-foreground: #1d1d1f; + --sidebar-border: rgb(0 0 0 / 0.08); + --sidebar-ring: #0071e3; + + /* Materials (frosted chrome) */ + --material-nav: rgb(251 251 253 / 0.8); + --material-nav-dark: rgb(22 22 23 / 0.8); /* nav while over the ink band */ + --material-thick: rgb(255 255 255 / 0.82); /* menus, popovers, sticky action bars */ + --material-thin: rgb(255 255 255 / 0.6); + --blur-material: saturate(180%) blur(20px); + --blur-overlay: blur(6px); + --overlay: rgb(0 0 0 / 0.32); + + /* Elevation: soft, wide, low-alpha */ + --elev-1: 0 1px 2px rgb(0 0 0 / 0.04), 0 4px 16px rgb(0 0 0 / 0.04); + --elev-2: 0 2px 4px rgb(0 0 0 / 0.04), 0 12px 32px rgb(0 0 0 / 0.08); + --elev-3: 0 0 0 0.5px rgb(0 0 0 / 0.08), 0 8px 30px rgb(0 0 0 / 0.12); + --elev-bar: 0 -0.5px 0 rgb(0 0 0 / 0.08); + --elev-thumb: 0 1px 3px rgb(0 0 0 / 0.12), 0 0 0 0.5px rgb(0 0 0 / 0.04); /* segmented-control thumb, switch knob */ + + /* Focus */ + --focus-ring: 0 0 0 4px rgb(0 113 227 / 0.28); + + /* Motion */ + --motion-ease-out: cubic-bezier(0.22, 1, 0.36, 1); + --motion-ease-standard: cubic-bezier(0.4, 0, 0.6, 1); + --motion-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --duration-instant: 120ms; + --duration-fast: 200ms; + --duration-base: 280ms; + --duration-slow: 400ms; + --reveal-distance: 16px; + + /* Layout & spacing */ + --nav-height: 48px; + --localnav-height: 52px; + --gutter: 16px; /* mobile side gutter */ + --gutter-lg: 24px; + --space-section-app: 56px; /* between Sections in the app */ + --space-section-marketing: clamp(88px, 11vw, 160px); + --space-card: 24px; /* card padding (20px under 640px) */ + --space-card-sm: 20px; + --space-stack: 16px; /* form field stack */ +} + +@media (prefers-reduced-motion: reduce) { + :root { --duration-fast: 0ms; --duration-base: 0ms; --duration-slow: 0ms; --reveal-distance: 0px; } +} + +/* Frosted material utilities (Tailwind v4 @utility) */ +@utility material-nav { + background-color: var(--material-nav); + -webkit-backdrop-filter: var(--blur-material); + backdrop-filter: var(--blur-material); + box-shadow: inset 0 -0.5px 0 var(--hairline); +} +@utility material-thick { + background-color: var(--material-thick); + -webkit-backdrop-filter: var(--blur-material); + backdrop-filter: var(--blur-material); +} +/* Fallback: browsers without backdrop-filter get an opaque surface so text stays legible. */ +@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { + .material-nav { background-color: #fbfbfd; } + .material-thick { background-color: #ffffff; } +} + +@layer base { + * { @apply border-border; } + html { font-family: var(--font-sans); color-scheme: light; -webkit-text-size-adjust: 100%; } + body { + background: var(--background); + color: var(--foreground); + font-size: 17px; line-height: 1.47059; letter-spacing: -0.022em; /* marketing/auth base */ + -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + font-feature-settings: "kern"; + } + :focus-visible { outline: none; box-shadow: var(--focus-ring); border-radius: inherit; } + ::selection { background-color: rgb(0 113 227 / 0.18); } + a { color: inherit; text-decoration: none; } + .prose-link, main p a:not([data-slot]) { color: var(--link); } + .prose-link:hover, main p a:not([data-slot]):hover { text-decoration: underline; text-underline-offset: 0.15em; } + button:not(:disabled), [role="button"]:not(:disabled) { cursor: pointer; } + body * { scrollbar-width: thin; scrollbar-color: #d2d2d7 transparent; } +} +``` + +## Typography + +FONT STACK AND WIRING (no SF font files): +- In src/app/layout.tsx, remove Geist and Geist_Mono. Use `import { Inter } from "next/font/google"; const inter = Inter({ subsets: ["latin"], variable: "--font-inter", display: "swap", axes: ["opsz"] });`. Put `inter.variable` on . `axes: ["opsz"]` loads Inter's optical-size axis, so display sizes tighten on their own. If the optical axis isn't in the installed next/font version, drop `axes`. +- The --font-sans and --font-display stacks (see tokens) start with `-apple-system, BlinkMacSystemFont`. Apple devices render the OS system font (which switches between Text and Display optical sizes by itself). Windows, Android and Linux fall through to Inter. "SF Pro Text"/"SF Pro Display" only match when the user has them installed locally. We never bundle or download them. +- Mono: `ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace`. Use it only for ids, JSON, code and trace output. +- Tracking differs by font. The values below are tuned for the system font. Inter renders slightly wider at display sizes, so add `html:not(.apple-font) .text-display* { letter-spacing: calc( - 0.01em) }`. Only do this if QA on Windows shows loose display headlines. It's optional, and skipping it is fine. +- `viewport.themeColor`: "#ffffff" for marketing/auth and "#f5f5f7" for the app (set it in (app)/layout via generateViewport). + +TYPE SCALE (desktop → mobile under 734px). Define each as a Tailwind v4 `@utility text-` in globals.css so pages never hand-roll sizes: +| Role | Use | Size/line-height | Weight | Tracking | +|---|---|---|---|---| +| display-xl | Marketing hero headline only | 80/84px → 48/52px | 600 | -0.015em | +| display | Marketing section headlines, final CTA | 56/60px → 40/44px | 600 | -0.005em | +| headline | Marketing sub-sections, auth H1, deliverable title, worker name on profile | 40/44px → 32/36px | 600 | -0.003em (auth/app: -0.02em) | +| title-1 | App page H1 (PageHeader) | 32/40px → 28/34px | 600 | -0.02em | +| title-2 | Section headings (Section), dialog titles, big card titles | 22/28px → 21/26px | 600 | -0.012em | +| title-3 | Card titles, worker names in cards, list primary line on marketing | 17/22px | 600 | -0.022em | +| body-lg (lead) | Marketing subheads, hero subhead, auth subhead | 21/29px → 19/27px | 400 | 0.004em | +| body | Marketing body, reading view, auth | 17/25px | 400 | -0.022em | +| body-app | App base text (set on the (app) shell container, not body) | 15/22px | 400 | -0.01em | +| callout | Dense rows, table cells, form help | 14/20px | 400 | -0.012em | +| footnote | Metadata, timestamps, secondary lines, nav links | 13/18px | 400 (500 for nav) | -0.006em | +| caption | Chart axes, legal, tiny labels | 12/16px | 400 | 0 | +| metric-xl | Big KPI numbers (usage headline, worker score) | 48/52px → 40/44px | 600 | -0.02em, tabular-nums | +| metric | Stat strip numbers | 34/40px → 28/34px | 600 | -0.02em, tabular-nums | + +RULES: +- Bump the app base text from 14px to 15px. It is the single biggest "feels premium" change. Dense tables and activity rows use callout (14px). +- Kill the uppercase `eyebrow` utility. Redefine it as sentence case at 13px/18px, weight 600, color var(--muted-foreground), with no letter-spacing. Marketing eyebrows ("New in scheduling") use 17px weight 600 in var(--warning) orange or foreground. Use them at most once per section. +- Weights are only 400 and 600, plus 500 for nav and pill labels. Never use 700 or 800 and never use light weights. Marketing step numbers may use 600 in var(--text-tertiary). +- `text-wrap: balance` on every heading. `text-wrap: pretty` on paragraphs. Paragraph max width is 692px (--container-text) on marketing and 65ch in the app. +- Keep the `metric` utility (tabular-nums), and apply it to every live-updating number and numeric column. +- Font size is never below 12px. Inputs are 16px under 640px (no iOS zoom), 15px in app, 17px on auth. + +## Navigation + +APP SHELL: replace the left sidebar with a global frosted top nav and page-level local navs. + +1. Global nav (new `src/components/shell/global-nav.tsx`; AppShell keeps its props: user, simulated, pendingApprovals) +- `position: sticky; top: 0; z-40; height: var(--nav-height)` (48px). Apply the `material-nav` utility (rgba(251,251,253,.8), saturate(180%) blur(20px), 0.5px bottom hairline). The inner container is max-w-[1200px] with px-4 sm:px-6. +- Left: the wordmark links to /workforce. The glyph is the existing three-node SVG, drawn in #1d1d1f at 20px with no colored tile. "AI Staffing Agency" is set in 15px weight 600 at -0.02em on one line. Drop the stacked "AGENCY" caps. +- Center-left (md+): primary sections as plain text links at 13px/500, gap-7. Rest color is #1d1d1f at 80% opacity. Hover goes to 100%. Active is 100% with weight 600, plus a 2px × 16px rounded underline 6px below the text, drawn in foreground (not blue). The links are Workforce, Jobs, Approvals and Activity. There are no icons. +- Approvals count: show a small 18px-high pill after the label when there are pending items. It is var(--warning-soft) with var(--warning) text, 11px/600 tabular-nums, and an aria-label like "3 pending approvals". This is the only colored element in the nav. +- Right cluster, gap-3: + - Simulated chip (when simulated). A 22px pill with bg #f5f5f7 at 0.5px var(--hairline), a 6px var(--warning) dot, and "Simulated" at 12px/500 in #6e6e73. The existing tooltip explanation stays. + - A "Hire" primary pill in the small size (h-7, px-3.5, 13px/500, blue). It is the global primary action; hide it on /hire. + - The user menu trigger: a 28px circular org monogram (bg #1d1d1f, white initials 11px/600). +- User menu: a DropdownMenu in the material-thick style with radius 14px. The header shows the org name at 15px/600 and the email at 13px secondary. Items are Usage, Settings, then a separator, then "Sign out". Usage and Settings move out of primary nav into this menu, keeping the bar to 4 destinations. +- The skip link remains the first focusable element. + +2. Mobile (under 768px) +- The same 48px frosted bar holds the wordmark (glyph plus "AI Staffing"), the Simulated dot-chip (dot only, with aria-label), and a two-line menu button (2 × 16px bars, 1.5px thick, 6px apart). The menu button morphs into an X over 240ms. +- Open state: a full-screen overlay below the bar with bg rgba(251,251,253,.96) and blur. It lists links at 28px/600 with -0.015em tracking and 12px vertical gap, 32px from the top, 16px gutters. The links are Workforce, Jobs, Approvals (count), Activity, then a 32px gap, then Usage and Settings at 17px/500 in secondary color. A full-width "Hire a worker" primary pill (h-12) sits at the bottom, with the org, email and "Sign out" in 13px. +- Links stagger in at 20ms apart with opacity 0→1 and translateY(-8px→0) over 280ms ease-out. With reduced motion there is no transform and the overlay appears instantly. Body scroll is locked while open. Close on route change (keep the current usePathname effect) and on Escape. Focus is trapped inside the overlay. +- The old left Sheet (MobileNav) is deleted, and so is the SidebarNav. + +3. Local nav (sub-navigation for pages with sections, like the product sub-nav on premium consumer sites) +- A new `src/components/shell/local-nav.tsx`. It is sticky under the global nav (`top: var(--nav-height)`), 52px tall, material-nav, full-bleed with the inner container at max-w 1200. +- Left: the object title at 21px/600 with -0.012em tracking (e.g., "Alex"). The title fades in (opacity 0→1, 200ms) once the page header scrolls out of view, using an IntersectionObserver on the H1. +- Right: section links at 13px/500. Active is foreground with weight 600. Inactive is #1d1d1f at 72% opacity. Add one optional small primary pill (e.g., "Run now"). +- Used on: the worker profile (tabs), job detail (Spec / Workers / Runs / Deliverables / Versions as in-page anchors), and run detail (Timeline / Evaluation / Debug anchors). +- Mobile: links scroll horizontally with `scroll-snap-type: x proximity` and a 24px fade mask on the right edge. The active link scrolls into view on load. + +4. Section headers inside pages +- PageHeader is left-aligned. An optional back link sits above the title ("‹ Workforce" at 14px in var(--link)), replacing the multi-level breadcrumb. The title is title-1, the description is 17px secondary (max 65ch), and actions sit right-aligned on desktop. +- Actions on mobile: the primary action moves to a sticky bottom bar. The bar is material-thick with a top hairline and safe-area padding, and holds a full-width pill. +- Section headers: title-2 with an optional 15px secondary description. The trailing action is a blue text link with a chevron ("View all ›"), not an outline button. + +5. Marketing nav (for "/" and the auth pages) +- The same 48px frosted bar: wordmark on the left. Centered 13px links: How it works, Product, Security, Pricing. They are in-page anchors with `scroll-margin-top: 64px`. +- Right: "Sign in" as a 13px text link and "Get started" as a small blue pill. A signed-in visitor sees an "Open Workforce" pill instead. +- While the bar overlaps the dark showcase band, it switches to --material-nav-dark with white text (IntersectionObserver on the section, 200ms color transition). +- Mobile: the same full-screen overlay pattern. + +6. Main container and z-order +- The app `
` gets bg var(--surface-secondary) (#f5f5f7), max-w-[1200px], px-4 sm:px-6, pt-10 pb-24, and body-app type. +- Z-order: global nav z-40, local nav z-30, sticky action bar z-30, dialogs z-50. +- Make "/", "/sign-up" and the marketing anchors public in `src/server/auth/access.ts` decideAccess. Today every page is gated and "/" redirects to /workforce. The middleware matcher stays the same. + +## Components + +Change shadcn primitives in src/components/ui/* with minimal class edits; the tokens do most of the work. Shared components in src/components/* are listed after. + +BUTTON (ui/button.tsx) +- Base: rounded-full, font-weight 500, tracking -0.01em. Transition background-color/box-shadow/opacity at 200ms ease-standard. +- Remove `active:translate-y-px`. Pressed state uses the bg -active token or opacity-90. +- Focus: the --focus-ring 4px halo (ring-4 ring-primary/30); no border change. +- Sizes: + - xs h-6 px-2.5 12px + - sm h-7 px-3.5 13px + - default h-9 px-4 14px (app) + - lg h-11 px-5.5 17px (auth, hire flow, marketing) + - xl h-12 px-7 17px (marketing hero CTAs; add it) + - Icon sizes: circular size-8 / size-9. Icon buttons are only for truly iconographic actions (close, overflow "…", copy). +- Variants: + - default: bg-primary text-white, hover:bg-primary-hover, active:bg-primary-active. + - secondary: the "gray pill", bg #e8e8ed text foreground, hover #dcdce1. The workhorse for non-primary actions. + - outline: kept for compatibility but restyled. bg white, 1px var(--input) border, hover bg #f5f5f7. Use it sparingly; prefer secondary. + - ghost: transparent, hover bg rgb(0 0 0 / .05). + - link: text var(--link), no underline at rest, underline on hover, h-auto p-0, optional trailing ChevronRight size-3.5 via `data-icon=inline-end`. + - destructive: text var(--danger) on bg var(--danger-soft), hover a slightly darker soft fill. ConfirmDialog keeps a solid red for the final confirm. +- Only one default-variant button per view. Disabled state is opacity 40%. + +CARD (ui/card.tsx) +- Base: bg-card rounded-[18px] (radius-xl) shadow-card. Remove `ring-1 ring-foreground/10`. Padding p-6 (p-5 under 640px). CardHeader gap-1.5, CardTitle title-3, CardDescription 14px secondary. +- CardFooter: no top border or tinted background. Actions are left-aligned or split with justify-between. +- Clickable cards: the whole card is the link target. Hover gets shadow-card-hover and translateY(-2px) at 280ms ease-out, and there's no underline on the title. +- Cards placed on a white surface (marketing white sections, dialogs) use bg #f5f5f7 with no shadow. Add a `variant="tile"` prop or a class convention for this. +- Inset panels inside cards: bg #f5f5f7 rounded-[12px] p-4, no border. + +INPUT / TEXTAREA / SELECT +- h-11 in the app (textarea min-h-28), rounded-[12px], bg white, 1px var(--input) border, px-3.5, text 16px under 640px and 15px above. +- Placeholder var(--text-tertiary). Focus: border-primary plus a 4px rgb(0 113 227/.15) halo. +- Invalid: border var(--danger), with a 13px danger message below and a leading 12px circle-alert icon. +- Auth pages use floating-label inputs: h-14, label at 17px secondary moving to 12px on top when focused or filled, 180ms. +- Select trigger matches inputs, with a trailing chevron-up-down 14px in secondary color. +- Labels: 14px/500 foreground, placed above with 6px gap. Help text 13px secondary. + +CHECKBOX / RADIO / SWITCH +- Checkbox 18px rounded-[5px], 1px #d2d2d7 border, checked bg-primary with a white check. +- Radio 18px circle, checked has a 6px white center on blue. +- Switch 28×48 track: off #e9e9eb, on primary. The 24px white knob gets --elev-thumb. Transition 200ms ease-standard. + +TABS (ui/tabs.tsx) +- Two looks. + - (a) Segmented control (TabsList default) for filters and 2–5 short options. Track bg #e8e8ed, rounded-full, p-0.5, h-8. The active trigger is a white pill with --elev-thumb, 13px/500. Inactive is 13px/500 foreground at 80%. The thumb slides between options with a 240ms transform (use layout via CSS anchor, or just accept an instant swap when motion is reduced). + - (b) Local-nav tabs (worker profile) are text links in LocalNav (see navigation), not TabsList. + +BADGE (ui/badge.tsx) and STATUS BADGE (status-badge.tsx) +- Default status rendering becomes a "status dot": a 7px dot plus a 13px/500 label in foreground, with no background and no ring. Tone colors: success, info (pulsing only while running), warning, danger, neutral. +- Only attention states get the soft pill (bg *-soft, text tone color, h-6 px-2.5 rounded-full): "Needs review", "Waiting for you", "Failed". +- Update the static maps in src/lib/status (TONE_CLASSES) to the new tokens: emerald→success, amber→warning, rose→danger, sky→info, slate→neutral. +- Rule: one status element per object in lists. + +SIMULATED BADGE +- A neutral 22px pill: bg #f5f5f7, 0.5px hairline, 6px var(--warning) dot, 12px/500 "Simulated" in #6e6e73, cursor help. The existing tooltip copy stays. Remove the flask icon and the dashed amber border. +- Placement: always in the global nav, and inline in the meta line of run, deliverable and usage artifacts ("Simulated run · 2.4s · $0.00 billed"). Remove it from Section action slots and from StatCard hints. + +SCORE RING (score-ring.tsx) +- 3px stroke on a #e8e8ed track, with a round linecap. The number is centered at 15px/600 tabular-nums for size 44, and at 28px for size 72. +- Band color applies to the arc only (≥80 success, 65–79 warning, <65 danger). The number stays in foreground. +- Null shows a dashed #d2d2d7 track and a "—" in tertiary. +- On the worker profile header, replace the ring with a metric-xl number plus a "Performance" caption and the band word ("Strong", "Watch", "At risk"). + +WORKER AVATAR +- Circle, sizes 32/40/48/72. Keep the static color map, but move every entry to a pastel fill (~92% lightness) with ~40%-lightness initials of the same hue, weight 600. Never saturated fills, never gradients, never emoji. + +STAT CARD (stat-card.tsx), rebuilt as StatStrip plus Stat +- The four KPIs sit in one white card, split into 4 columns (2×2 under 768px) separated by 1px var(--border) vertical hairlines. Each cell has 24px padding. +- Each cell: label at 13px/500 secondary; value in metric (34px/600, -0.02em); optional hint at 13px secondary. +- The trend line is a 13px word in the tone color ("↑ 12% vs last month"), with no icon component. +- Remove the corner icons. Keep the sparkline only on spend: 80×28, 1.5px stroke in foreground at 40% opacity, no fill. + +SECTION (section.tsx) +- The h2 uses title-2; the description is 15px secondary; spacing is space-y-5. +- The action slot expects ``. +- Stack sections at gap var(--space-section-app) (space-y-14). + +PAGE HEADER +- An optional back link (`backHref`, `backLabel`) replaces the breadcrumbs prop. Keep breadcrumbs support but render only the parent as "‹ Parent". Show more than one level only on deep pages, as "Workforce › Alex" in 13px. +- h1 uses title-1; the description is 17px secondary; margin-bottom 40px. +- Actions: at most one primary plus one secondary pill, with the rest in a "…" DropdownMenu (a circular icon button with bg #e8e8ed). + +EMPTY STATE +- Center-aligned with py-20. An optional 32px line icon in var(--text-tertiary) with no circle background. Title in title-2, description at 17px secondary (max 44ch), then one pill CTA (default size lg). +- Drop the dashed border box. Inside a card, the card itself is the frame. + +DATA TABLE / TABLE +- The table sits inside a card with p-0 and overflow hidden. +- Header row: 13px/600 secondary, sentence case, no background, bottom hairline, h-11, px-6. +- Rows: min-h-14, 15px, hairline dividers inset 24px from the left (not full-bleed), hover bg #f9f9fb. +- Numbers right-aligned in tabular-nums. The first column is the human label in weight 500. +- Row click navigates. There's no chevron column; instead the title turns var(--link) on hover. +- Under 640px, tables become stacked lists: title on line 1, meta on line 2. + +DIALOG / CONFIRM DIALOG +- Overlay var(--overlay) with backdrop blur(6px). +- Content: bg white, rounded-[22px], p-7, shadow-popover, max-w-[440px]. Enter with a fade plus scale .96→1 at 240ms ease-out; exit 160ms. +- Title in title-2, body 15px secondary. +- Footer: pills right-aligned, secondary then primary. Under 640px they stack full-width with the primary on top. +- ConfirmDialog destructive confirm: a solid bg var(--danger) white-text pill. + +SHEET +- Used only for the review panel on mobile and the run-step details. It's a bottom sheet on mobile (rounded-t-[22px], a 36×5 grab handle in #d2d2d7) and a right panel at 420px on desktop, with shadow-popover. + +DROPDOWN MENU / POPOVER / SELECT CONTENT +- material-thick, rounded-[14px], shadow-popover (includes the 0.5px edge), p-1.5. +- Items: h-9, rounded-[8px], px-3, 14px. Hover bg rgb(0 0 0/.05). Icons at 16px in secondary color, and only where they aid scanning. +- Separators: 1px var(--border) with mx-3 my-1.5. +- Animation: fade plus translateY(-4px→0) at 180ms. + +TOOLTIP +- bg rgb(29 29 31/.92) with blur, white 12px text, rounded-[8px], px-2.5 py-1.5, max-w-64, delay 300ms. + +TOAST (sonner) +- Drop `richColors` (colored toast backgrounds read as generic). White material-thick, rounded-[14px], shadow-popover, 15px title, 13px secondary description. +- The status shows as a 16px tone-colored icon only. Position is bottom-center on mobile and top-right on desktop. + +ACCORDION / COLLAPSIBLE +- Full-width rows, hairline between items, 17px/600 trigger, and a chevron-down (14px, secondary) that rotates 180° over 240ms. Content fades in with height auto (use the Radix CSS var). + +PROGRESS +- 4px tall, rounded-full, track #e8e8ed, fill primary. The indeterminate state is a 30% bar sliding at 1.2s ease-in-out, and it is static under reduced motion. + +SKELETON +- bg #ececf0, radius matched to the content (18px cards, 8px text lines), gentle opacity pulse 0.6↔1 over 1.6s. No shimmer gradient. + +SEPARATOR +- 1px var(--border). Prefer whitespace over separators. + +MARKDOWN (deliverable body) +- An `article` class: max-w-[720px], body 17px/1.6. h1 is 32px, h2 22px with a 40px top margin, h3 17px/600. Lists 17px with 8px item gap. Tables follow the table style. Blockquote uses a 3px #d2d2d7 left rule (the one allowed left rule, because it's typographic) and secondary text. Links use var(--link). Code blocks: bg #f5f5f7, rounded-[12px], 13px mono. + +JSON VIEW / COPY BUTTON +- JSON: bg #f5f5f7, rounded-[12px], 13px mono, p-4, with a max-h and internal scroll. +- Copy button: a circular 28px ghost button in the top-right corner, with a 1.2s "Copied" tooltip. + +CHARTS (recharts: usage-chart, cost-chart, performance-chart, sparkline) +- Series lines are 2px with monotone curves. The area fill is the series color at 8% opacity fading to 0. +- Dots only on hover (r 4, white 2px stroke). No vertical grid. Horizontal grid is 1px var(--chart-grid) with 4 ticks max. +- Axis ticks are 12px in var(--chart-axis). No axis lines. +- Tooltip is material-thick at radius 12 with the value in 15px/600. +- Bars get a 6px top radius, max bar width 28px. +- Legends are inline text with 8px dots, placed above the chart and right-aligned. + +RELATIVE TIME, AUTO REFRESH +- No visual change; timestamps are always 13px secondary. When AutoRefresh is active, show a subtle live dot in the page meta ("Live"), not a spinner. + +HIRE STEPPER (hire/_components/stepper.tsx) +- Replace the chip stepper with a 13px "Step 2 of 5 · Clarify" label in secondary color, over a 5-segment progress rail (4px tall, 4px gaps, rounded-full). Completed segments are foreground, current is primary, upcoming is #e8e8ed. No emerald checks. + +LOGO +- Monochrome glyph (#1d1d1f, 20px), no tile, no shadow, no ring. The favicon uses the same glyph as black on white. + +## Pages + +GLOBAL FOR THE APP +- The canvas is #f5f5f7 and content sits in white 18px cards. +- The container is 1200px with 40px top padding. Sections are 56px apart. +- Every page opens with PageHeader (title-1). Only one primary pill per page. +- Remove any Section-level SimulatedBadge, corner icons on stats, and nested bordered boxes. + +1. MARKETING LANDING "/" +- Public route. Built from server components, plus one small client `` island for scroll animation. +- Section-by-section structure and copy are in the landingPage field. +- Implementation notes: + - New files: `src/app/(marketing)/page.tsx` (replacing `src/app/page.tsx`'s redirect), `src/app/(marketing)/layout.tsx` (marketing nav plus footer), and `src/app/(marketing)/_components/*` (hero, steps, feature-row, showcase, trust, pricing-teaser, cta, footer, and mock cards). + - Signed-in visitors see "Open Workforce" in the nav instead of a redirect. + - Add `/`, `/sign-up`, `/privacy` and `/terms` to the public allowlist in access.ts. + - Page metadata: an original title/description plus OpenGraph. The OG image is generated with next/og from the headline in brand type, black on white. + +2. SIGN-IN (/sign-in) +- Delete the dot-grid backdrop, the radial glow and the BriefcaseBusiness tile. +- Layout: white page, marketing nav on top (wordmark only, plus "Create account" at right). The content column is max-w-[400px], centered, with pt-[12vh]. +- Hierarchy: 40px glyph → H1 "Sign in" (headline, 40px) → 17px secondary "Pick up where your team left off." → floating-label Email (h-14) → floating-label Password with a show/hide text toggle ("Show") inside the field → "Keep me signed in" checkbox (optional) → primary pill "Sign in" (lg, full width, h-11) → a centered 14px link "Forgot password?" (only if the reset flow ships; otherwise omit it) → hairline → 15px "New to AI Staffing Agency? Create an account ›". +- Errors: one inline alert above the fields (bg danger-soft, 14px danger text, rounded-12). Never reveal whether the email exists. +- Session-expired: a neutral info alert, "Your session ended. Sign in again to continue." +- Demo credentials: remove the prefill entirely in production. Only when `NODE_ENV !== "production"` or a DEMO_MODE env flag is set, show a secondary pill "Explore the demo workspace" below the form that fills and submits. +- Footer: 12px secondary, © year, Privacy, Terms. + +3. SIGN-UP (/sign-up), new +- The same shell as sign-in. +- H1 "Create your account", sub "Your first worker can be on the job today." +- Fields: Full name, Work email, Company name (becomes the organization), Password with live requirement text "At least 12 characters". The requirement text is 13px secondary and turns success-colored with a check once met. +- A checkbox: "I agree to the Terms and Privacy Policy" (links). +- Primary "Create account". Below, "Already have an account? Sign in ›". +- After sign-up, go to /hire with a one-time welcome line at the top of the hire page: "Welcome, Dana. Let's hire your first worker." +- Field-level errors appear under each field. The submit button shows a spinner and "Creating account…" while pending. + +4. WORKFORCE (/workforce) +- Header: "Workforce", sub "Your AI workers and how they're doing." Action: "Hire a worker" primary. The global nav already has Hire, so on this page make the header action a secondary "Hire a worker" only if the nav one is hidden. Pick one: keep the nav pill and remove the header button on desktop. +- Order: + - (1) "Needs you". Show only when there are items. It's a single white card titled "3 things need you" (title-2 inside the card). Rows are avatar 32 + sentence ("Alex wants approval to send the weekly digest to 4 recipients") + a right-aligned "Review" secondary pill. Max 3 rows, then "See all ›". No amber strip, no colored background. + - (2) Stat strip: one card, four stats (Active workers, Runs today, Waiting for review, Spend this month). No icons. + - (3) "Your team": the worker grid, 3 columns xl, 2 md, 1 mobile, gap-6. + - (4) "Recent activity": a card with 6 rows and "View all ›". +- Worker card redesign: + - Top row: avatar 48 + name (title-3) + title (14px secondary). The score (44px ring) is on the right. + - One status line at 13px ("● Running now" / "● Idle · next run tomorrow 9:00" / "● Paused"). + - A 2×2 fact grid as plain text: 13px secondary label over a 15px value. No icons, no inset gray box. + - Footer: "Run now" secondary pill (sm) + "View profile ›" link. + - Remove the three-badge row, the job-title third line (put it in a tooltip or on the profile) and the healthReason paragraph (it becomes the status line in warning color: "● Needs attention · quality dipped last 3 runs"). +- The first-run empty state is a large centered hero inside the canvas: 40px "Hire your first AI worker", 17px description, lg primary pill, and 3 example-job gray pills that deep-link to /hire?prefill=…. + +5. HIRE FLOW (/hire) +- A focused single column at max-w-[720px], centered, with the stepper label at the top. Remove any side content. +- Step 1 "Describe": + - H1 in headline size: "What do you need done?" + - Sub: "Describe it like you'd brief a new contractor: the outcome, how often, and who it's for." + - A large white card containing a borderless textarea at 19px/1.5 with min-h 200px and placeholder "Every Monday, summarize what our three main competitors shipped last week and email it to the product team." A 13px secondary character hint sits at the bottom right. + - Below the card: "Try an example" with 4 gray pills. + - Primary lg pill "Continue", right-aligned (on mobile, full-width in a sticky bottom bar). +- Clarify: + - One question per card. The question is 17px/600. Options are selectable tiles (radius 14, 1px #d2d2d7, selected has a 2px primary border plus a check at top-right). The free-text answer uses the same input style. + - "Back" is a text link at left; "Continue" is primary at right. +- Spec review: render the JobSpec as a document. + - Title in headline size, then sections Outcome / Deliverable / Schedule / Sources / Success criteria, each with a 17px/600 heading, 17px body and an "Edit" link that opens inline editing. + - A sticky bottom material bar holds "Looks right, design my worker" (primary) and "Discard" (link, danger color, with confirm). +- Proposal pending: centered, a 44px progress ring (indeterminate, static when motion is reduced), the headline "Designing your worker…", and 3 status lines that cross-fade every 2.5s (deterministic text from the flow state, not random). +- Proposal: a "résumé" card (white, radius 22, p-8). + - Avatar 72, name in headline size, title in 21px secondary, and a 17px bio paragraph. + - Three columns under a hairline: "Skills" (plain list), "Tools & access" (each tool as a sentence, e.g. "Can search the web" or "Asks before sending email"), and "Expected cost" (metric number + "per run, estimated"). + - Actions: "Hire Alex" primary lg + "Ask for changes" secondary. Alternates appear as smaller cards below under "Other candidates". +- Remove the emerald check chips, multicolor step rings and dense proposal-details grids. + +6. JOBS (/jobs) +- Header "Jobs", sub "Everything you've asked for, and who's on it." +- A segmented control filter (All · Active · Draft · Closed) replaces the chips. +- The table sits in a card with columns Job (title 15/500 + one-line summary 13 secondary), Workers (overlapping avatars 28, max 3 + "+2"), Status (dot), and Updated (relative, right-aligned). Mobile uses stacked rows. +- Job detail: a back link, then the H1 job title with a status dot line ("● Active · created Mar 3"). Actions: "Hire another worker" secondary and "…" (Close job, Discard). +- LocalNav anchors: Spec, Workers, Runs, Deliverables, History. +- Two columns on lg (8/4). The main column holds the spec as a document card (same as hire spec review, read-only, with an "Edit spec" link creating a new version). The side column holds a "Workers on this job" card (avatar rows + score) and a "Recent runs" card (5 rows). +- Spec versions go in an accordion at the bottom titled "Version history". + +7. WORKER PROFILE (/workers/[id]), 9 tabs +- Header (no card, directly on the canvas): + - Back link "‹ Workforce". Avatar 72, then the name in headline size (40px), then the title in 17px secondary, then "Hired for Weekly competitor digest ›" in var(--link) at 15px. + - One status line at 13px. + - Right side on desktop: a metric-xl score + "Performance · Strong", then the actions "Run now" (primary), "Talk to Alex" (secondary) and "…" (Pause, Edit schedule, Replace, Retire; Retire in danger color with confirm). + - A health callout only when it needs attention: a warning-soft rounded-14 panel, "Alex's last three reviews dipped below 65. See performance ›". +- LocalNav: + - Visible links: Overview, Activity, Deliverables, Performance, and "Talk to Alex" (display label; keep the WORKER_TABS ids and the frozen WORKER_TAB_LABELS, and override the display in tab-nav.tsx). A "More" dropdown holds Cost, Permissions, Versions and Debug. + - When the active tab lives in More, the More trigger reads "Cost ▾" in weight 600. + - Mobile shows all 9 as a horizontal scroll with no More menu. +- Per tab: + - Overview: an "About Alex" card (bio 17px + what they do each run as a numbered plain list), a stat strip (Runs, Success rate, Avg score, Cost this month), a "Latest deliverable" preview card (title, first 3 lines, "Read ›"), and a "Schedule" card ("Every Monday at 9:00, next in 3 days"). + - Activity: a feed grouped by day. The day header is sticky at 13px/600 secondary. Rows are sentences with the time at right. + - Deliverables: a list card with rows of title, date, review state (dot) and score (right). + - Performance: a hero chart card (score over time with 65/80 reference lines as 1px dashed #d2d2d7 labeled "Watch" / "Strong"), then criteria as horizontal 6px bars in a card, then review history. The primary action is "Write a performance review". + - Talk to Alex: the chat fills the viewport height minus the navs, max-w 760 centered. The user's bubbles are right-aligned, bg primary with white text. The worker's bubbles are left-aligned on bg #e9e9eb. Bubbles are radius 18, 15px/1.4, with 4px spacing inside a group and 16px between groups. Timestamps are 12px tertiary between groups. The composer is a material-thick sticky bottom bar with a rounded-full input (h-11) and a circular 32px primary send button with an arrow-up icon. Suggested prompts show as gray pills when the chat is empty. + - Cost: a metric-xl "$4.12 this month" headline + "Simulated · priced, not billed" when applicable, then a bar chart, then a breakdown table by model tier and tool. + - Permissions: an inset grouped list (white card, rows at h-16). Each tool is a sentence title + 13px explanation, and the right side holds a switch or an "Asks first" segmented option (Allowed / Asks first / Off). Save happens in a sticky bar when dirty. The schedule editor goes in a separate card. + - Versions: a timeline list with "Current" as a 13px/600 success text tag on the active version. Each row shows the version number, date, a change summary sentence and the score. The "Propose an improvement" card sits at the top with a secondary action. The compare action opens the Replace page. + - Debug: visually demoted. A 15px secondary intro, "Raw model calls and tool traces, for troubleshooting." Collapsed accordions with mono JSON views, no charts. + +8. RUN DETAIL (/runs/[id]) +- Back link "‹ Alex". H1: "Monday digest run" (or "Run on Mar 3, 9:00"). The meta line is 13px: "● Completed · 2m 14s · $0.03 · Simulated". +- Live state: a 2px primary progress rail pinned under the global nav, a pulsing 7px info dot, and the status line "Working, step 4 of ~7". Keep AutoRefresh and RunLive. +- LocalNav anchors: Timeline, Deliverable, Evaluation, Debug. +- Timeline: one white card holding a vertical list with a 1px #e5e5ea connecting line at x=20 and 9px step nodes (success/info/warning/danger fill). Each step is a human sentence at 15px ("Alex searched the web for 'competitor X changelog'"), with duration and tokens at 13px secondary on the right. Clicking expands an inset #f5f5f7 panel (radius 12) with the details (inputs/outputs; JSON in mono). +- A pending approval is rendered inline as an elevated card (shadow-card-hover): "Alex needs your OK to send an email", a preview, and "Approve" (primary) / "Decline" (secondary). No colored left border. +- Evaluation cards: the score as metric + per-criterion bars + reviewer notes as a 17px quote. +- The debug trace goes in a collapsed accordion. +- Actions: "Retry" or "Cancel run" (secondary), plus "…". + +9. DELIVERABLE (/deliverables/[id]) +- Reading view on a WHITE background (not the canvas) to feel like a document. +- A centered article at max-w 720: back link, then the title in headline size, then the meta "By Alex · Mar 3 · Score 84 · Simulated". Actions below the title: "Download" secondary pill and "Copy" link. +- The body uses the Markdown article styles. +- Review panel: on lg it's a sticky right column (w-[320px], top = nav+24px) with a white card containing "How did Alex do?". It has a 5-option rating segmented control, a feedback textarea, and "Accept" (primary) / "Request changes" (secondary). On mobile it's a sticky bottom bar with a "Review" pill that opens the bottom Sheet. +- After a decision, show a quiet line: "Accepted by you · Mar 3". + +10. APPROVALS (/approvals) +- Header "Approvals", sub "Actions your workers won't take without you." +- A segmented control: Waiting (count) · Decided. +- Waiting: a stack of cards (max-w 820). Each card: + - Avatar 40 + a 17px/600 sentence: "Alex wants to send an email to 4 people". + - 13px secondary: "Requested 12 min ago · Weekly competitor digest". + - An inset preview panel (recipients, subject, first lines) with "Show full ›". + - Buttons: "Approve" primary + "Decline" secondary. Decline opens a small textarea for a reason. +- The empty state reads "You're all caught up." with 17px secondary "New requests will appear here and in the nav." +- Decided: the table (Request, Worker, Decision dot, By, When). + +11. ACTIVITY (/activity) +- Header "Activity". +- Filters: a segmented control (All · Runs · Deliverables · Decisions) plus a worker Select (gray pill trigger). +- The feed is grouped by day with sticky day headers inside one card per day. Rows: avatar 32 + sentence + 13px time on the right, row height 56. Infinite load with a "Show more" secondary pill (no auto infinite scroll). + +12. USAGE (/usage) +- Header "Usage" with a range segmented control (7 days · 30 days · 90 days) in the actions slot. +- A hero card: metric-xl "$12.40", caption "spent in the last 30 days", "↓ 8% vs previous period" in tone. In simulated mode, a 13px note reads "Simulated. Costs are priced for reference, nothing is billed." +- The chart card is full width. +- Two cards in a 2-column grid: "By worker" and "By model tier" tables. +- The billing card is simplified to the plan name, the period and a "Manage billing" secondary. Hide it until billing exists. + +13. SETTINGS (/settings) +- A layout inspired by a desktop system-preferences window. + - lg: a left list column (220px) of plain text links (Workspace, Members, Model providers, Credentials, Runtime, Danger zone), with the active one in a white rounded-10 pill. The right column (max-w 720) holds the grouped inset lists. + - Mobile: a single list that drills into each section. +- Each group is a white card of rows (label 15px left; value, control or chevron right; hairlines inset 24px), with the group title at 13px/600 secondary above the card. +- Credentials: never display secrets. Show "Added Mar 3 · ends in ••••4f2a" plus "Replace" and "Remove" links. +- Provider cards: a status dot + "Simulated" or "Live". +- Danger zone: a separate card with a red text button, and typed-confirmation in the dialog. + +14. REPLACE (/workers/[id]/replace/[versionId]) +- A side-by-side comparison like a product compare page. +- H1 "Replace Alex with version 3?", sub "Here's what changes. Alex's history stays with the old version." +- Two columns (Current | Proposed), each headed by avatar 48, name, and a version tag. Aligned rows follow: Model tier, Instructions summary, Tools & access, Schedule, Expected cost per run, Recent score or projected. +- Rows that differ get a 6px primary dot with "Changed" at 12px on the proposed side. Unchanged values are secondary text. +- Mobile: the columns stack, with each row showing "Before → After". +- A sticky bottom material bar: "Replace Alex" primary + "Keep current version" secondary. Confirm via dialog. + +15. LOADING / ERROR / NOT-FOUND +- Loading: skeletons that mirror the final layout (title bar 32px, stat strip, cards). No spinners. +- Error: a centered title-2 "Something went wrong on our side.", 17px secondary copy, "Try again" primary + "Go to Workforce" link. No stack traces in production. +- 404: "We couldn't find that page." with the same layout. + +## Public landing page (`/`) + +Public marketing page at "/". All copy below is original to this brand. Layout follows the premium-consumer rhythm: full-bleed sections alternating white (#fff) and light gray (#f5f5f7), centered headlines, and generous vertical padding (var(--space-section-marketing)). Content width is 1024px and the text column is 692px. Every visual is HTML/CSS mock cards built from the product's own components, with static sample data, wrapped in `role="img"` plus a descriptive aria-label (or aria-hidden with an adjacent caption). There are no images, gradients, blobs or emoji. + +0. NAV (frosted, 48px, sticky) +- Wordmark "AI Staffing Agency". +- Links: How it works · Product · Security · Pricing. +- Right: "Sign in" (text) and "Get started" (small primary pill). + +1. HERO (white; pt 120px, pb 0; centered) +- Headline (display-xl): "Describe the job. Meet your new hire." +- Subhead (body-lg, secondary, max 640px): "Tell us what needs doing in plain English. We scope the work, design an AI worker for it, and put them on a schedule. Every deliverable is reviewed, scored, and yours to keep. If it isn't working, you replace them in a click." +- CTAs (xl pills, gap 16): "Get started" (primary) and "See how it works" (secondary gray pill, scrolls to #how). +- Beneath, 13px tertiary: "No credit card required." +- Showcase frame starts 64px below the CTAs. It's a rounded-[36px] #f5f5f7 frame, 1024px wide, that bleeds into the next section (negative bottom margin). Inside is a mock Workforce page: + - A mini frosted nav. + - A stat strip (Active workers 6 · Runs today 14 · Waiting for review 2 · Spend this month $38.20). + - Three worker cards with invented names: Maya, "Market research analyst", score 88; Theo, "Support inbox triager", score 81; Priya, "Lead list builder", score 74 with a warning dot. +- Motion: the frame rises 24px and fades in over 600ms on load (the one longer animation, skipped under reduced motion). + +2. HOW IT WORKS (#how; gray; 3 columns on md, stacked on mobile) +- Headline (display): "Three steps from idea to output." +- Each step has a large number (48px/600, tertiary color), a title-3 and a 17px secondary body: + - 1, "Write the brief." "A few sentences is enough. We'll ask a couple of sharp questions, then turn it into a clear job spec you can edit." + - 2, "Hire your worker." "Review a candidate's skills, tools, and expected cost before they start. Approve, and they're on the schedule." + - 3, "Review the work." "Each run produces a deliverable you can read, rate, and send back. Scores build up into an honest performance record." +- Under each step, a small mock: a brief textarea, a résumé card, a deliverable card with a score ring. + +3. FEATURE: RESUME BEFORE THE FIRST RUN (#product; white; text left, mock right on lg; stacked on mobile) +- Eyebrow (17px/600, warning orange): "Hiring" +- Headline (headline): "See exactly who you're hiring." +- Body: "Every worker comes with a résumé: what they'll do on each run, which tools they can use, what they're allowed to do without asking, and what it should cost. No black boxes." +- Link: "How workers are designed ›" +- Mock: a proposal résumé card for "Maya, Market research analyst". Skills list; tools as sentences ("Searches the public web", "Asks before emailing anyone"); "About $0.04 per run". + +4. FEATURE: EVERY RUN, NARRATED (gray; mock left, text right) +- Headline: "Every run, told in plain English." +- Body: "Follow along step by step: what was searched, what was read, what was written. When something goes sideways, you'll know where and why." +- Mock: a run timeline with 5 human sentences and durations, one step expanded into an inset panel. + +5. FEATURE: YOU APPROVE WHAT MATTERS (white; text left, mock right) +- Headline: "Nothing sensitive happens without you." +- Body: "Decide which actions need your sign-off. Sending an email, posting an update, touching a customer record: your worker pauses, shows you the draft, and waits." +- Mock: an approval card, "Theo wants to reply to 3 customers", with Approve / Decline pills. + +6. SHOWCASE BAND (the only dark section; bg #000, text #f5f5f7; full-bleed; pt/pb 160px) +- Headline (display, white): "Your whole team, on one page." +- Sub (body-lg, #a1a1a6): "Who's working, what's waiting on you, and what it's costing, updated as it happens." +- A large mock: a Workforce plus performance chart composition on a #1d1d1f frame with radius 36. The chart is an SVG-free recharts-like line built from inline SVG paths, with a static blue line and gray grid. +- The nav switches to its dark material while over this band. + +7. FEATURE: PERFORMANCE AND REPLACE (gray; two tiles side by side, radius 28, white, p-10) +- Tile A: headline "Performance reviews, not guesswork." Body: "Scores for accuracy, completeness, and usefulness, tracked across every run. Spot a slide before it becomes a problem." Mock: a score chart with Strong/Watch reference lines. +- Tile B: headline "Not working out? Replace them." Body: "Propose an improved version, compare it side by side, and switch over. The full history stays on file." Mock: the Before → After comparison rows. + +8. TRUST AND SECURITY (#security; white; centered headline + 2×3 grid of short items; no icons, or single 24px monochrome line icons only) +- Headline (display): "Built to be trusted with real work." +- Sub: "Your workers act on your behalf, so the guardrails are part of the product, not an add-on." +- Items (title-3 + 15px body): + - "Permissions enforced on our servers": "Tool access is checked on every action, not just shown in the interface." + - "Approval before sensitive steps": "Actions you mark as sensitive wait for a person." + - "Your workspace, walled off": "Every record is scoped to your organization, and every request is checked." + - "A complete audit trail": "Every run, tool call, and decision is logged and reviewable." + - "Secrets stay secret": "Credentials are stored encrypted and never shown again after you add them." + - "Costs you can see": "Every run shows its cost to the cent." +- Note for the implementer: publish only claims that are true at launch. Verify each item against docs/SECURITY.md and the security workstream. Drop "encrypted" unless credential encryption at rest ships. Link "Read our security overview ›" to /security once it exists. + +9. PRICING TEASER (#pricing; gray) +- Headline: "Pay for the work, not the seats." +- Sub: "Start free, then pay for what your workers actually do. Every run is itemized." +- Three white cards (radius 22), each with the name (title-2), one-line description, price placeholder, 4 plain bullet lines and a pill: + - "Starter": "For trying out your first worker." + - "Team": "For teams putting several workers on real jobs." (visually emphasized with a 2px primary border; no "Most popular" ribbon) + - "Enterprise": "For custom controls, SSO, and volume." +- Implementer note: the owner must supply real prices. Until then, render "Pricing coming soon" and route the CTAs to /sign-up and a contact mailto. Never invent numbers in production. + +10. FINAL CTA (white; centered; pt/pb 160px) +- Headline (display): "Your next hire is a paragraph away." +- Pills: "Get started" (primary xl) and "Sign in" (secondary xl). + +11. FOOTER (gray #f5f5f7; 12px/16px, #6e6e73; hairline top) +- Four columns (collapsing into accordions on mobile): + - Product: How it works, Security, Pricing. + - Company: About, Contact. + - Resources: Help center, Status. + - Legal: Privacy, Terms. +- Bottom row: "© 2026 AI Staffing Agency. All rights reserved." at left and "Made for teams who'd rather review than repeat." at right. +- Don't link pages that don't exist. Omit them until built. + +Motion on the landing page: each section's headline, body and mock get the Reveal treatment (fade + 16px rise, 400ms ease-out, 60ms stagger, once per element, threshold 0.2). + +## Motion + +PRINCIPLES +- Motion confirms cause and effect or eases content in, and nothing else. Transitions run 120–400ms. No springs with overshoot, no parallax, no scroll-jacking, no auto-playing carousels, and no infinite decorative loops. +- Easing tokens: + - --motion-ease-out cubic-bezier(0.22,1,0.36,1) for entering or revealing. + - --motion-ease-standard cubic-bezier(0.4,0,0.6,1) for state changes and hovers. + - --motion-ease-in-out cubic-bezier(0.65,0,0.35,1) for things that move and settle (the segmented thumb). +- Duration tokens: instant 120ms (press feedback), fast 200ms (hover, color, menus), base 280ms (dialogs, card hover lift, nav overlay), slow 400ms (scroll reveals). The single 600ms exception is the hero showcase frame. +- Animate only opacity and transform, plus background-color and box-shadow for hovers. Never animate width, height or top (the accordion uses the Radix height var). + +SPECIFICS +- Buttons: bg-color over 200ms standard. Pressed state is opacity .9 for 120ms. No translate or scale. +- Clickable cards: shadow-card → shadow-card-hover and translateY(-2px) over 280ms ease-out. +- Global nav: shadow and hairline always present. When the page scrolls past 8px, the material opacity goes .72→.8 over 200ms (optional). +- Mobile menu: the icon bars rotate into an X over 240ms. The overlay fades over 280ms. Links stagger 20ms each with translateY(-8px→0). +- Local nav title: fades in over 200ms when the H1 leaves the viewport. +- Dialog: overlay fades over 200ms. Content does opacity 0→1 plus scale .96→1 over 240ms ease-out; exit is 160ms. +- Dropdown/popover: opacity plus translateY(-4px) over 180ms. +- Sheet: slides from the bottom or right over 320ms ease-out. +- Toasts: sonner defaults, capped at 280ms. +- Segmented control: the thumb translates between options over 240ms in-out. Tabs content cross-fades over 160ms. +- Live states: + - The running dot is 7px info with a 1.6s ease-out ring pulse (scale 1→2.2, opacity .5→0). It's the ONLY infinite animation allowed, and it's shown only while a run is in flight. + - The live progress rail advances with a width transition over 400ms. +- Skeleton: opacity pulse 0.6↔1 over 1.6s. No shimmer. +- Number changes (stat values on AutoRefresh): no count-up animation. Values swap in place with tabular-nums so nothing jitters. +- Scroll reveal (marketing only; never inside the app): + - A client component `src/app/(marketing)/_components/reveal.tsx` using one shared IntersectionObserver (threshold 0.2, rootMargin "0px 0px -10% 0px"). It sets `data-revealed` once and then unobserves. + - CSS: `[data-reveal]{opacity:0;transform:translateY(var(--reveal-distance));transition:opacity var(--duration-slow) var(--ease-out),transform var(--duration-slow) var(--ease-out);transition-delay:calc(var(--i,0)*60ms)} [data-reveal][data-revealed]{opacity:1;transform:none}` + - Progressive enhancement: the hidden initial state applies only under `html.js` (set by a tiny inline script in the marketing layout, or `@media (scripting: enabled)`). Content is always visible without JS, for crawlers and in thumbnails. +- Reduced motion: + - `@media (prefers-reduced-motion: reduce)` sets every duration token to 0ms and --reveal-distance to 0, so reveals appear instantly. + - It also disables the running-dot pulse (static dot), the indeterminate progress slide (static 30% bar) and the hero rise. + - Also add `*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}` as a global safety net. + - Smooth anchor scrolling (`scroll-behavior:smooth` on html) applies only under `prefers-reduced-motion: no-preference`. + +## Things that make a UI look AI-generated — and what we do instead + +"AI-GENERATED LOOK" TELLS, AND THE REMEDY FOR EACH + +1. Purple or indigo accent (the current --primary is oklch indigo-violet). Remedy: a single blue #0071e3 for actions and links only, and near-black text for everything else. +2. Gradient blobs, radial glows, dot-grid backdrops and "aurora" meshes (the current auth layout has a dot grid plus a primary/4% glow). Remedy: flat white or #f5f5f7 surfaces. Depth comes only from soft shadows and scale of type. +3. Emoji or the "✓" glyph used as UI (the hire stepper uses "✓"), plus sparkle icons for "AI". Remedy: no emoji anywhere, and no Sparkles icon to mean "magic". Say what happens in words. +4. Lucide icon soup, meaning an icon on every label, stat, fact and nav item (WorkerCard facts, StatCard corners, sidebar). Remedy: icons only where they're faster than a word (close, more, search, send, copy, back chevron), at 16px in secondary color. Nav and labels are text only. +5. Badge overload (worker cards show status + health + run badges + a Simulated badge on the section). Remedy: one status per object as a dot plus word, health merged into the score, and Simulated shown globally once plus inline on generated artifacts. +6. Borders on everything, nested bordered boxes (card ring → bg-muted/40 bordered fact grid) and dashed outlines. Remedy: cards with no border and a soft shadow on a gray canvas, inset panels as flat #f5f5f7 fills, and hairlines only between list rows. +7. Equal-weight everything (14px text everywhere, 20px page titles barely larger than body, 4 identical KPI tiles). Remedy: a steep type scale (32px page titles, 22px sections, 15px body) and one clear focal point per page. Group KPIs into one strip. +8. Uppercase tracked micro-labels ("WORKSPACE", "AGENCY", the `eyebrow` utility). Remedy: sentence-case 13px semibold secondary labels. Use eyebrows only on marketing, once per section. +9. Dense cards stuffed with label:value grids. Remedy: lead with one human sentence, demote metadata to 13px secondary, and move detail into the profile. +10. Generic SaaS copy ("Supercharge your workflow", "Unlock the power of AI", "Seamless", "Revolutionize"). Remedy: concrete, contractor-style copy that names the outcome ("Every Monday, a digest of what competitors shipped"). +11. Colored tinted toasts and alert boxes with left accent borders. Remedy: white material toasts with a small tone icon. Callouts are soft-tinted panels with no side stripe. +12. Rainbow status palette used liberally (emerald, amber, rose and sky all at once on one screen). Remedy: status color only in 7px dots and short text, with fills reserved for the single "needs you" state. +13. Too many primary buttons and outline buttons in every header. Remedy: one blue pill per view, gray pills for secondary actions, and chevron text links for tertiary ones. Overflow goes into a "…" menu. +14. Stock "AI" illustrations, robots, brains, glowing orbs and 3D renders. Remedy: real product UI as HTML/CSS mock cards, with names and data that look like a real team's work. +15. Tiny sidebar-first dashboard chrome (a 240px sidebar that eats width, nav in 13px). Remedy: a frosted top nav with 4 destinations, a user menu for the rest, and full-width content up to 1200px. +16. Over-rounded AND over-shadowed controls (shadow-xs on buttons, ring-inset on logo tiles). Remedy: pills have no shadow. Shadow is reserved for cards, menus and dialogs. +17. Shimmer skeletons, count-up numbers, bouncing entrances and confetti. Remedy: the restrained motion spec, with reduced motion honored. +18. "Most popular" ribbons, fake testimonials, fake logos walls and invented metrics ("10x faster", "Trusted by 5,000 teams"). Remedy: none until they're real. The trust section states verifiable product guarantees only. +19. Inconsistent radii (6, 8, 10, 12, 16px mixed) and spacing. Remedy: the radius roles (inputs 12, cards 18, dialogs 22, tiles 28, hero 36) and spacing tokens, enforced through primitives rather than per-page classes. +20. Copying another company's look literally (its logo, product names, product photography, exact headline phrasing, SF font files). Remedy: an original wordmark and glyph, original copy, system fonts and Inter only, so we borrow the sensibility and nothing else. + +IMPLEMENTATION GUARDRAILS +- Update src/components/README.md's "Visual language" table to this system. Change the "Linear / Vercel feel" and "indigo" guidance, and remove the rule against touching ui primitives for this redesign pass. +- Keep class names static (no `bg-${tone}`). The redesigned TONE_CLASSES map references the new token utilities (text-success, bg-warning-soft…). +- Don't introduce dark mode in this pass. Keep `@custom-variant dark` class-scoped as today. +- After the change, run an axe or contrast check. #86868b must never be used for body text under 18px. + +## Implementation notes (wave B, design-foundation) + +Two things bite silently if you don't know them, so they are settled once in the foundation: + +- **`cn()` must know the type scale.** tailwind-merge reads any `text-` as a text *colour*, so an unconfigured `cn("text-footnote", "text-muted-foreground")` drops the size. `src/lib/utils.ts` exports a `createCn(...)` instance that registers every `@utility text-*` role as a font size. **Import `cn` from `@/lib/utils`, never from the bare `cn` package** — `tests/ui/cn.test.ts` enforces both halves. +- **The font stacks keep a literal fallback: `var(--font-inter, "Inter")`.** An unresolved `var()` makes the whole `font-family` invalid at computed-value time and the page renders in the browser's default serif. The fallback matters for any tree rendered outside the root `` — `app/global-error.tsx` replaces the root layout and therefore never gets next/font's variable. +- Focus is one base rule (`:focus-visible { box-shadow: var(--focus-ring) }`). A component that already carries a `shadow-*` utility would replace it, so those add `focus-visible:ring-4 focus-visible:ring-primary/30` instead (Tailwind's ring and shadow compose). The base rule does not set `border-radius: inherit`: a box-shadow already follows the element's own radius, and inheriting one would reshape elements that have none. +- Toasts sit top-center (one position, no viewport-dependent switch), which is the one place this implementation diverges from the "bottom-center on mobile, top-right on desktop" line above. diff --git a/docs/PRODUCTION.md b/docs/PRODUCTION.md new file mode 100644 index 0000000..27e0bf6 --- /dev/null +++ b/docs/PRODUCTION.md @@ -0,0 +1,101 @@ +# Production & security phase — contracts and ownership + +This phase makes the MVP publicly deployable and hardened, and redesigns the UI (see `docs/DESIGN.md`). +It supersedes the "FROZEN" list in `docs/CONTRACTS.md` **only** for the files explicitly assigned below. +Audit findings that motivate each item live in the lead's audit reports (IDs like F-002, INF-06, OPS-01). + +## Already done by the lead (frozen for this phase) +- `package.json`: npm `overrides` (undici/postcss/deepmerge-ts) → `npm audit` = 0 vulnerabilities; `shadcn` moved to devDeps; `esbuild` added; scripts `worker`, `build:worker`, `start:worker`, `db:seed:demo`, `audit:prod`. Agents may edit `scripts` only if their brief says so; never run `npm install`. +- `prisma/schema.prisma` + migration `20260922090000_production_security` (applied to dev + test DBs): `Organization.{isDemo, monthlyBudgetUsd, maxConcurrentRuns, maxQueuedRuns, maxActiveWorkers, suspendedAt}`, `User.{sessionVersion, passwordChangedAt, lastSignInAt, disabledAt, emailVerifiedAt}`, models `RateLimitBucket`, `SecurityEvent` (+ enum `SecurityEventType`), `Invitation`, `OrgSpendMonth`, `ExecutorHeartbeat`, and hot-path indexes. The existing demo org (`acme-robotics`) was marked `isDemo = true`. +- `src/server/env.ts` — `validateEnv()` / `assertValidEnv()` (zod; production rules). Call `assertValidEnv()` at web boot (instrumentation) and worker boot. +- `src/server/config.ts` — new getters: `isProduction`, `appVersion`, `publicUrl`, `executor.mode ("inline"|"off")` — dev-only switch for whether `npm run dev` also starts a worker; the web process never executes runs, `executor.shutdownGraceMs`, `auth.*` (signupMode, signupInviteCode, demoMode, allowDemoSeed, session ages, trustedProxyHops, password policy numbers, bcryptRounds, invitationTtlDays), `limits.*` (rateLimitDisabled, defaultMonthlyBudgetUsd, platform ceilings, maxInFlightRunsPerWorker), `retention.*`, `log.*`. +- `src/server/auth/permissions.ts` — `PERMISSIONS`, `can(role, p)`, `assertCan(session, p)` (throws FORBIDDEN with a human message), `permissionsFor(role)`. Pure; any server module may deep-import it. +- `next.config.ts` — `output: "standalone"`, `poweredByHeader: false`, static security headers (HSTS in prod, nosniff, frame DENY, referrer, permissions, COOP, CORP), server-action `bodySizeLimit: 256kb` + optional `SERVER_ACTIONS_ALLOWED_ORIGINS`. +- `tests/setup/env.ts` sets `RATE_LIMIT_DISABLED=true` and `EXECUTOR_MODE=off`; rate-limit tests delete it locally. + +## Role matrix (enforced inside server modules via `assertCan`) +| Permission | Min role | Enforced in | +|---|---|---| +| workers.run | MEMBER | workers.startRun, runtime.retryRun, runtime.cancelRun | +| workers.chat | MEMBER | workers.sendMessageToWorker | +| deliverables.review | MEMBER | evaluation.recordDeliverableFeedback | +| approvals.decide | MEMBER | runtime.decideApproval (all approvals) | +| approvals.decideExternal | ADMIN | runtime.decideApproval when the tool's `sideEffect === "external_write"` (re-read the deciding user's role from DB) | +| reviews.generate | MEMBER | evaluation.generatePerformanceReview | +| jobs.manage | ADMIN | staffing.scopeJob/buildJobSpec/updateJobSpec/approveJobSpec/reviseJobSpec/discardJob/proposeWorker, jobs close action | +| workers.hire | ADMIN | staffing.hireWorker, workers.hireReplacement | +| workers.manage | ADMIN | workers.pause/resume/retire/updateSchedule/updateToolGrant/proposeReplacement/rejectProposedVersion/activateVersion | +| credentials.manage | ADMIN | secrets.setCredential/deleteCredential | +| members.invite | ADMIN | account.createInvitation/revokeInvitation (can invite MEMBER or ADMIN, never OWNER) | +| members.manage | OWNER | account.changeMemberRole/removeMember | +| org.manage | OWNER | account.updateOrgSettings (name, monthly budget) | + +## `@/server/security` (owner: **security**) — API other modules code against +```ts +export type RateRule = { name: string; limit: number; windowSec: number }; +export const RATE_RULES: { + signInAccount: RateRule; // 5 / 15 min per sha256(email) — lockout with exponential backoff (15 min × 2^lockCount, max 24 h) + signInIp: RateRule; // 30 / 15 min per client IP + signUpIp: RateRule; // 5 / hour per IP + inviteAcceptIp: RateRule; // 20 / hour per IP + invitesCreateOrg: RateRule;// 30 / day per org + passwordChangeUser: RateRule; // 5 / hour per user + llmUser: RateRule; // 30 / 10 min per user — every action that calls an LLM + llmOrg: RateRule; // 300 / day per org + runOrg: RateRule; // 60 / hour per org — Run now, retry, hire-with-first-run + credentialsOrg: RateRule; // 20 / hour per org + pollUser: RateRule; // 240 / min per user — GET /api/runs/[id] +}; +export async function hit(rule: RateRule, subject: string): Promise<{ allowed: boolean; count: number; retryAfterSec: number }>; +export async function enforce(rule: RateRule, subject: string): Promise; // throws AppError("LIMIT_EXCEEDED", "You're doing that too often. Try again in N s.") +export async function isLocked(rule: RateRule, subject: string): Promise<{ locked: boolean; retryAfterSec: number }>; +export async function lock(rule: RateRule, subject: string): Promise<{ lockedUntil: Date }>; // exponential backoff via lockCount +export async function resetLimit(rule: RateRule, subject: string): Promise; +export async function sweepRateLimits(now?: Date): Promise; // delete buckets idle > 2 days +// All limiter functions are no-ops (allowed) when config.limits.rateLimitDisabled; non-auth rules fail OPEN on DB errors (log), auth rules fail CLOSED. + +export async function recordSecurityEvent(e: { type: SecurityEventType; organizationId?: string; userId?: string; email?: string; ip?: string; userAgent?: string | null; metadata?: Record }): Promise; // never throws; hashes email; clips UA +export async function listSecurityEvents(organizationId: string, opts?: { userId?: string; limit?: number }): Promise; // plain JSON +export async function requestContext(): Promise<{ ip: string; userAgent: string | null }>; // next/headers; safe outside a request (→ "unknown") +export function clientIpFromHeaders(headers: Headers, trustedHops?: number): string; +export function hashEmail(email: string): string; + +export async function assertOrgActive(organizationId: string): Promise; // suspendedAt → FORBIDDEN "This workspace is suspended…" +export async function assertWithinBudget(organizationId: string): Promise; // month-to-date REAL spend (OrgSpendMonth) ≥ budget → LIMIT_EXCEEDED +export async function getBudgetStatus(organizationId: string): Promise<{ month: string; spentUsd: number; budgetUsd: number; remainingUsd: number; exceeded: boolean }>; +export async function recordRealSpend(organizationId: string, costUsd: number, tx?: DbOrTx): Promise; // atomic upsert-increment of OrgSpendMonth (UTC month) +export function clampRunLimits(limits: RunLimits): RunLimits; // platform ceilings from config.limits + +export function redactSecrets(text: string): string; // sk-…, tvly-…, AIza…, Bearer …, long hex/base64 tokens → [redacted] +export function publicErrorMessage(e: unknown): { message: string; ref: string }; // safe for clients; logs internals with ref +``` +Dependency position: `security` imports only `db`, `errors`, `config`, `domain`, Prisma types and `next/headers` (lazily). `usage` may import it (recordRealSpend); everything above may import it. + +## `@/server/account` (owner: **accounts**) — API +```ts +export function checkPasswordPolicy(password: string, ctx?: { email?: string; name?: string; organizationName?: string }): { ok: true } | { ok: false; reason: string }; +export async function signUp(input: { name: string; email: string; password: string; organizationName: string; inviteCode?: string }, ctx: { ip: string; userAgent: string | null }): Promise<{ userId: string; organizationId: string }>; +export async function changePassword(s: SessionContext, input: { currentPassword: string; newPassword: string }): Promise; // bumps sessionVersion +export async function signOutEverywhere(s: SessionContext): Promise; // bumps sessionVersion +export async function listMembers(organizationId: string): Promise; +export async function changeMemberRole(s: SessionContext, userId: string, role: UserRole): Promise; // OWNER; never leaves the org without an OWNER; bumps target sessionVersion +export async function removeMember(s: SessionContext, userId: string): Promise; // OWNER; disabledAt + sessionVersion bump; cannot remove self/last owner +export async function createInvitation(s: SessionContext, input: { email: string; role: "MEMBER" | "ADMIN" }): Promise<{ invitationId: string; inviteUrl: string; expiresAt: string }>; +export async function listInvitations(organizationId: string): Promise; +export async function revokeInvitation(s: SessionContext, invitationId: string): Promise; +export async function getInvitationByToken(token: string): Promise<{ organizationName: string; email: string; role: UserRole; expiresAt: string } | null>; +export async function acceptInvitation(token: string, input: { name: string; password: string }, ctx: { ip: string; userAgent: string | null }): Promise<{ email: string }>; +export async function getOrgSettings(organizationId: string): Promise; +export async function updateOrgSettings(s: SessionContext, input: { name?: string; monthlyBudgetUsd?: number | null }): Promise; // OWNER +``` +Invitation links are shown to the inviting admin to share (no email provider in Phase 1). Self-service password reset needs an email provider — documented as a known gap. + +## File ownership this phase +**Wave B (parallel)** +- **accounts** — `src/server/auth/**` (not `types.ts`, `permissions.ts`), `src/types/next-auth.d.ts`, `src/server/account/**`, `src/app/(auth)/**` (actions/schema + minimal functional `/sign-up` and `/invite/[token]` pages; wave C restyles), `src/app/(app)/settings/account-actions.ts`, `tests/auth/**`, `tests/account/**`. +- **security** — `src/server/security/**`, `src/middleware.ts`, `src/lib/action-result.ts`, `src/lib/markdown.ts`, `src/server/secrets/**`, `src/server/tools/**` (not `types.ts`/`schemas.ts`), `src/server/models/**` (not `types.ts`), `src/app/(app)/deliverables/[deliverableId]/download/route.ts`, `src/app/api/runs/[runId]/route.ts`, `docs/SECURITY.md`, `tests/security/**` + tests for the modules above. +- **guardrails** — `src/server/staffing/**`, `src/server/workers/**`, `src/server/runtime/**` except `executor.ts`, `src/server/evaluation/**`, `src/server/usage/**`, `src/server/activity/**`, every `actions.ts` / `manage-actions.ts` under `src/app/(app)/**` (rate limits + permission-aware results only — no UI changes), `src/server/queries/**` (adding `permissions` to view models), tests for those areas. +- **ops** — `src/worker.ts`, `src/instrumentation.ts`, `src/server/runtime/executor.ts`, `src/server/maintenance/**`, `src/server/log/**`, `src/app/api/health/route.ts`, `src/app/api/ready/route.ts`, `Dockerfile`, `.dockerignore`, `docker-compose.yml`, `.github/workflows/**`, `.env.example`, `.env.test.example`, `docs/DEPLOYMENT.md`, `prisma/seed.ts` + `prisma/seed/**`, `package.json` `scripts` only, `tests/ops/**`, `tests/seed/**`. +- **design-foundation** — `src/app/globals.css`, `src/app/layout.tsx`, `src/app/global-error.tsx`, `src/app/not-found.tsx`, `src/app/(app)/{layout,error,not-found,loading}.tsx`, `src/components/**` (incl. restyling `ui/*` primitives), `src/components/README.md`, `src/lib/**` except `action-result.ts`/`markdown.ts`, `src/server/queries/shell.ts`, `src/app/(dev)/styleguide/**`, `docs/DESIGN.md`, `tests/ui/**`. + +**Wave C (after B)** — page redesigns per `docs/DESIGN.md`; assignments in the wave-C briefs. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 439d91a..ae109ee 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -1,86 +1,159 @@ -# Security & Production Readiness +# Security -Principle: **treat every AI worker as an untrusted contractor, and every client input (including LLM output) as hostile.** No control relies on a system prompt. Server-side enforcement only. +Principle: **treat every AI worker as an untrusted contractor, and every input — including model output, fetched web pages and anything a client sends — as hostile.** No control relies on a system prompt. Every rule is enforced server-side, inside the module that owns the data. + +This document describes what is *implemented*, with file pointers, plus the runbooks and the risks we are knowingly carrying. Audit finding ids (`F-…`, `INF-…`, `OPS-…`) refer to the production-readiness audits for this phase. + +--- ## 1. Threat model -Assets: tenant business data (jobs, deliverables, run history), credentials/API keys, model spend (money), platform integrity. +**Assets.** Tenant business data (jobs, specs, deliverables, run traces, chat), stored tool credentials, model spend (real money on platform keys), and platform integrity/availability. + +**Trust boundaries.** + +| Boundary | Crossing | Treated as | +|---|---|---| +| Browser → app | Server actions, route handlers, Auth.js | Hostile input; session identity is re-read from the database on every request | +| App → model provider | `src/server/models/**` only | Untrusted output; nothing it returns is an instruction | +| Model → tools | `tools.invoke()` only | Every call re-authorized against the worker version's grants | +| Tool → internet | `src/server/tools/net-guard.ts`, `guarded-http.ts` | SSRF-hostile; addresses re-checked at connect time | +| Web page → model context | `fetch_url` / `web_search` output | Data, never instructions (indirect prompt injection) | + +**Adversaries.** + +- *Unauthenticated attacker*: credential stuffing, session forgery, IDOR probing, DoS on expensive endpoints. +- *Authenticated tenant (malicious or compromised)*: cross-tenant reads, cost abuse, privilege escalation inside the workspace. +- *Hostile web content*: a page that tells the worker to exfiltrate what it has collected, or to poison a deliverable. +- *The worker itself (excessive agency)*: runaway loops, unbounded spend, unintended external side effects. +- *Operator error / supply chain*: leaked keys, vulnerable dependencies, a misconfigured deployment. + +--- + +## 2. Controls + +### 2.1 Identity and sessions + +- Auth.js v5 credentials provider; bcrypt (cost `config.auth.bcryptRounds`), identical failure message for unknown-user and wrong-password, and a timing-equalizer hash so a missing account costs the same as a wrong password — `src/server/auth/authorize.ts`. +- JWT sessions: 12 h sliding, 7 d absolute, `sessionVersion` in the token. `loadSessionContext` re-reads the user on every request and rejects a token whose `sessionVersion` is behind, or whose user is disabled — `src/server/auth/session-context.ts`. A password change, "sign out everywhere", a role change or a removal bumps it, which revokes every existing cookie. +- Sign-in throttling and lockouts (F-002): 5 failures / 15 min per `sha256(email)` and 30 / 15 min per client IP, then an exponential lockout (window × 2^n, capped at 24 h). Both are checked *before* the password compare, and the account rule is applied whether or not the account exists, so lockouts are not an account oracle — `src/server/security/rate-limit.ts` + `src/server/auth/authorize.ts`. +- Self-serve sign-up is gated by `SIGNUP_MODE` (`open` / `invite` / `closed`, closed by default in production). Invitations are single-use, expiring, and stored as `sha256(token)` — `src/server/account/**`. +- The demo workspace is refused at sign-in unless `DEMO_MODE=true` (F-001). + +### 2.2 Authorization and tenant isolation + +- Every query and mutation is scoped by `organizationId` taken from the session, never from the request. A child resource is verified through `where: { id, organizationId }`; a mismatch is `NOT_FOUND`, never `FORBIDDEN` — no existence oracle. Ids are `cuid()`. +- Roles (MEMBER / ADMIN / OWNER) are enforced inside the server modules with `assertCan(session, permission)` — `src/server/auth/permissions.ts`. The UI only *hides* what you may not do; the server *refuses* it. Deciding an approval for an `external_write` tool re-reads the deciding user's role from the database. +- Route handlers answer `401`/`404` JSON instead of redirecting, and never leak whether an id exists in another workspace — `src/app/api/runs/[runId]/route.ts`, `src/app/(app)/deliverables/[deliverableId]/download/route.ts`. + +### 2.3 Rate limits, spend caps and the kill switch + +- One Postgres-backed fixed-window limiter (`RateLimitBucket`), one atomic `INSERT … ON CONFLICT DO UPDATE` per hit, so counts are correct across instances and concurrent requests — `src/server/security/rate-limit.ts`. Rules: sign-in (account/IP), sign-up, invite accept/create, password change, LLM per user and per org, runs per org, credentials per org, and the run-page poll endpoint (429 + `Retry-After`). +- Failure policy: **auth rules fail closed**, everything else fails open and logs. The whole limiter is a no-op when `RATE_LIMIT_DISABLED=1` (refused in production by `env.ts`). +- Monthly **real** spend per workspace is tracked in `OrgSpendMonth` (UTC month, atomic increment) and enforced against `Organization.monthlyBudgetUsd ?? PLATFORM_DEFAULT_MONTHLY_BUDGET_USD` — `src/server/security/budget.ts`. Simulated runs cost nothing and are never blocked. +- `assertOrgActive` is the operator kill switch (`Organization.suspendedAt`): no runs, no LLM calls. +- Both checks are repeated as a backstop immediately before every LIVE model call, so a code path that forgets them still cannot spend — `src/server/models/index.ts`. +- Per-run ceilings (cost, tool calls, duration) are clamped to platform maxima by `clampRunLimits`, and per-worker/per-org in-flight run caps live in the runtime queue. + +### 2.4 The worker as an untrusted contractor + +- **Single choke point.** `tools.invoke()` is the only execution path: registry lookup → Zod validation → `authorize` (tool in the blueprint *and* a non-revoked grant for this worker version) → approval check by `toolCallId` → execute → usage record. The runtime and the UI never call a tool's `execute()` — `src/server/tools/invoke.ts`. +- **Approvals.** `send_notification` (`sideEffect: "external_write"`) defaults to requiring approval; the run pauses and a human sees the exact payload. Only an `APPROVED` row for *that* `ToolCall` in *that* workspace lets it run; `PENDING`, `REJECTED`, `EXPIRED` and "no row" all refuse. Regression test: `tests/security/tool-provenance.test.ts`. +- **SSRF.** Scheme and port allow-lists, DNS resolution with private/reserved/metadata ranges blocked, redirects capped and re-validated per hop, and the *connection* re-checked at connect time so DNS rebinding cannot reach a blocked address — `src/server/tools/net-guard.ts`, `guarded-http.ts`. +- **Exfiltration via `fetch_url` (F-010).** In live mode a URL is capped at 2,048 characters, its fragment is stripped, and the host must have *provenance* in this run: it appeared in this run's own `web_search` results, or literally in the job brief / spec / this run's instructions. Anything else is refused with a `TOOL_ERROR` telling the model to search first — `src/server/tools/provenance.ts`. Simulated mode is unaffected. +- **No dynamic execution.** No `eval`, no `Function()`, no shelling out; `calculator` is a hand-written expression parser. +- **Bounded context.** Tool outputs are size-capped and schema-validated before re-entering the model context; trace payloads are clipped field-by-field. + +### 2.5 Output safety + +- Markdown is parsed into an AST and rendered as React elements — no HTML string is ever produced, no `dangerouslySetInnerHTML`, and link targets are restricted to `http(s)`, `mailto:` and same-site paths — `src/lib/markdown.ts`, `src/components/markdown.tsx`. +- The parser is hardened against pathological input (INF-17): no quadratic regexes, bounded look-ahead and recursion, a 2,000-character per-line block-scan limit and a 200 KB document cap. Regression tests assert a time budget on adversarial input — `tests/security/markdown.test.ts`. +- CSV output escapes formula prefixes (`= + - @`, tab, CR). +- Deliverable downloads are served inert: `nosniff`, `Content-Security-Policy: sandbox; default-src 'none'`, `Cache-Control: private, no-store`, and a sanitized ASCII `filename` plus RFC 5987 `filename*` — `src/app/(app)/deliverables/[deliverableId]/download/route.ts`. + +### 2.6 Browser and transport + +- Static headers on every response (HSTS in production, `nosniff`, `X-Frame-Options: DENY`, referrer, permissions, COOP, CORP, no `X-Powered-By`) — `next.config.ts`. +- A per-request nonce-based **Content-Security-Policy** with `strict-dynamic`, `object-src 'none'`, `base-uri 'self'`, `form-action 'self'`, `frame-ancestors 'none'` and `upgrade-insecure-requests` in production — `src/server/security/csp.ts` + `src/middleware.ts`. Redirect and 401 responses get an inert deny-all policy. `style-src` keeps `'unsafe-inline'` because React SSR emits style attributes and the toast library injects a stylesheet; there is no raw-HTML rendering path, so that residual is small. +- Server actions: Next.js origin checking (do not add a wildcard `SERVER_ACTIONS_ALLOWED_ORIGINS`) plus a 256 KB body limit. Every export of a `"use server"` file is a public endpoint, so `tests/security/server-actions.test.ts` asserts that each one is an async function that starts from `requireSession()` (with a named allow-list for sign-in, sign-up, invite acceptance and sign-out), and that route handlers export `GET` only (F-017). + +### 2.7 Secrets + +- Tool credentials are encrypted with AES-256-GCM under `CREDENTIAL_ENCRYPTION_KEY`. The envelope is `v2::::`, where `kid` identifies the key and the GCM **AAD binds the ciphertext to `organizationId:name`** — moving a row to another workspace or renaming it makes it undecryptable (F-014) — `src/server/secrets/crypto.ts`. +- Values are never logged, never returned by any list endpoint, and `last4` is only kept for values of 12+ characters. +- A credential that cannot be decrypted does **not** fall back to the platform env key (that would silently spend the platform's own key): the tool drops to Simulated mode and the operator sees `secrets.unreadable_credential` — `src/server/secrets/vault.ts`. +- `setCredential` / `deleteCredential` require `credentials.manage` (ADMIN), are rate limited per workspace, and write `CREDENTIAL_SET` / `CREDENTIAL_DELETED` audit events carrying the credential *name* only. +- Model-provider keys are process-wide environment variables, never per-tenant in Phase 1. `env.ts` refuses to boot in production with a missing/weak `AUTH_SECRET`, a bad `CREDENTIAL_ENCRYPTION_KEY`, a non-HTTPS `AUTH_URL` or a database URL without TLS. + +### 2.8 Audit trail and logging hygiene + +- `SecurityEvent` records sign-ins, throttles, lockouts, sign-outs, password and role changes, invitations, credential changes, rate-limit blocks and budget stops. It stores `sha256(email)` — never the address — the client IP, a 256-character user agent, and metadata with secret-looking keys and values scrubbed — `src/server/security/audit.ts`. `recordSecurityEvent` never throws. +- The client IP is read from the **right** of `X-Forwarded-For` using `TRUSTED_PROXY_HOPS`, never the leftmost (client-controlled) value — `src/server/security/request.ts`. +- Logs are structured JSON lines with clipped, `redactSecrets`-scrubbed fields; error *objects* are never logged whole (Prisma metadata can contain row values) — `src/server/security/log.ts`, `redact.ts`. +- Tenant-facing errors never carry internals: `runAction` passes through only the AppError codes whose message is written for users, and turns everything else into one generic sentence plus a `(ref xxxxxx)` that appears in the log line — `src/lib/action-result.ts`, `src/server/security/public-error.ts`. Provider text (request ids, echoed prompts, keys) stays in `ModelCall.error` and `AppError.details`, scrubbed. + +### 2.9 Retention + +`RETENTION_TRACE_DAYS` (default 30) clears `ModelCall` request/response payloads; `RETENTION_EVENTS_DAYS` (default 365) deletes activity and security events; rate-limit buckets idle for two days are swept (`sweepRateLimits`). The maintenance tick runs them — `src/server/maintenance/**`. + +--- -Adversaries & vectors: +## 3. Runbooks -- **External attacker (unauthenticated):** credential stuffing, session forgery, IDOR probing, endpoint scraping, DoS on expensive LLM endpoints. -- **Authenticated tenant (malicious or compromised):** cross-tenant access via id guessing, cost abuse, injection via job descriptions/chat, arbitrary tool invocation attempts. -- **Hostile web content (indirect prompt injection):** fetched pages/search results instructing the agent to exfiltrate data, call unauthorized tools, or poison deliverables. -- **The worker itself (excessive agency):** runaway loops, unbounded spend, unintended side effects. -- **Supply chain / operator error:** leaked secrets, vulnerable dependencies, misconfigured headers. +### 3.1 Rotating `CREDENTIAL_ENCRYPTION_KEY` -## 2. Authentication & session +1. Generate a key: `openssl rand -base64 32`. +2. Deploy with `CREDENTIAL_ENCRYPTION_KEY=` **and** `CREDENTIAL_ENCRYPTION_KEY_PREVIOUS=`. Both old (`v1`/`v2`) and new rows decrypt; new writes use the new key. +3. Re-encrypt every stored credential: `reencryptAll()` from `@/server/secrets` (idempotent, batched; rows already on the active key are skipped). It reports `{ scanned, reencrypted, failed }`. +4. Confirm `failed === 0`, then remove `CREDENTIAL_ENCRYPTION_KEY_PREVIOUS` and redeploy. Any `failed` row must be re-entered by a workspace admin in Settings. +5. Keep an offline escrow copy of the key, separate from database backups — losing it means losing every stored credential. -- Auth.js v5 credentials provider; passwords hashed with **bcrypt cost 12**; constant-time comparison (bcrypt native); identical error message for unknown-user vs wrong-password (no user enumeration). -- JWT session cookies: `httpOnly`, `sameSite=lax`, `secure` in production, 30-day max age with rolling refresh; session payload carries `userId`, `organizationId`, `role` — resolved server-side at issue time, never trusted from the client afterward for authorization decisions beyond identity. -- `middleware.ts` gates all `(app)` and `/api` routes (except auth + health); unauthenticated → redirect/401. -- Login endpoint rate-limited (per-IP + per-email token bucket: 5 attempts/min, backoff). CSRF on auth routes handled by Auth.js; server actions get Next.js origin/host verification (enabled by default) — we additionally set `allowedOrigins` explicitly. -- Phase 2: optional TOTP 2FA, OAuth (Google/Microsoft), org invitations with signed, expiring tokens, session revocation list. +### 3.2 Rotating `AUTH_SECRET` -## 3. Authorization & tenant isolation +Deploy with the new secret; Auth.js accepts an array, so keep the previous value in `AUTH_SECRET_PREVIOUS` for one release to avoid signing everyone out, then drop it. To force a global sign-out instead, increment every `User.sessionVersion`. -- Every query/mutation scoped by `organizationId` from `requireSession()` — **never from request payload**. Child resources verified through the parent chain in a single `where { id, organizationId }`; mismatch → `notFound()` (404, not 403 — no existence oracle). -- All ids are `cuid()` — non-sequential, non-enumerable. -- Role model (owner/admin/member) exists from day 1; MVP enforces membership; destructive ops (terminate worker, resolve approvals) require admin+ in Phase 2 RBAC tightening. -- No client-side authorization: UI hides what you can't do, server rejects what you may not do. -- Automated test coverage: cross-tenant access attempts for every query helper and action must return not-found (see TESTING.md). +### 3.3 Suspending a workspace / stopping spend -## 4. Tool-layer security (the core of "AI worker as untrusted contractor") +`UPDATE "Organization" SET "suspendedAt" = now() WHERE id = '…';` — runs stop being claimed, LLM calls refuse, the UI explains why. To cap instead of stop, set `monthlyBudgetUsd`. -- **Single choke point:** `tools.invoke()` is the only execution path. It (1) loads the `WorkerToolGrant` for the exact WorkerVersion, (2) rejects non-granted tools — even if the LLM asks, (3) Zod-parses args against the tool's schema (unknown keys stripped, sizes bounded), (4) enforces approval gates, (5) records an immutable `ToolCall` audit row. UI/runtime never call a tool's `execute()` directly. -- **Approval gates:** side-effectful tools (`send_email`, future CRM writes) default `requiresApproval: true`; the run pauses (`waiting_approval`), a human approves/rejects with the exact proposed payload displayed; approvals expire (24h → run fails safe). -- **SSRF defense** (`tools/http-guard.ts`) for `fetch_url` / future `http_request`: scheme allowlist (http/https), DNS-resolve then block private/reserved ranges (127/8, 10/8, 172.16/12, 192.168/16, 169.254/16 incl. cloud metadata 169.254.169.254, ::1, fc00::/7), block localhost aliases, cap redirects (3, re-validated per hop), response size cap (2 MB), timeout (10 s), text content-types only. -- **No dynamic execution:** no `eval`, no `Function()`, no shelling out; `calculator` uses a hand-rolled safe expression parser over numbers/operators only. -- **Output-side injection safety:** CSV generation escapes `= + - @ \t` cell prefixes (formula injection); deliverable markdown rendered with a sanitizing renderer (no raw HTML pass-through, no `dangerouslySetInnerHTML` of model output); JSON deliverables rendered as data, never executed. -- Tool results are size-capped and schema-validated before re-entering model context. +### 3.4 Incident response -## 5. LLM-specific security (OWASP LLM Top 10 mapping) +1. **Contain.** Suspend the affected workspace (3.3). For a suspected credential compromise, delete the credential and rotate the provider key at the provider. +2. **Revoke sessions.** Increment `sessionVersion` for the affected users (all sessions for that user stop on their next request). +3. **Assess.** `listSecurityEvents(organizationId)` for the actor's trail (sign-ins, IPs, credential and role changes); `ModelCall` / `ToolCall` / `ActivityEvent` for what the worker did; `UsageRecord` and `OrgSpendMonth` for spend. +4. **Find the error.** A user-reported `(ref xxxxxx)` appears verbatim in the JSON log line that carries the real cause. +5. **Rotate.** `CREDENTIAL_ENCRYPTION_KEY` (3.1), `AUTH_SECRET` (3.2), provider keys, and the database password if the host was reachable. +6. **Record.** Timeline, blast radius (which workspaces, which data), fix, and the regression test that keeps it fixed. -- **LLM01 Prompt injection:** fetched web/search content is demarcated as untrusted data in prompts; tool allowlists + grants are enforced server-side, so injected "instructions" cannot expand capability; structured outputs Zod-validated; agent never sees or handles secrets. -- **LLM02 Insecure output handling:** every model output parsed/validated before use (`generateObject` + schema; free text sanitized at render). Model output never becomes SQL, shell, HTML, or tool args without validation. -- **LLM04 Model DoS / cost abuse:** hard budgets per run (max steps/model calls/tool calls/tokens/wall-clock from blueprint), per-org concurrency cap and daily USD cap (org settings, enforced at enqueue + per model call), rate limits on scoping/chat/run-now actions. -- **LLM06 Sensitive info disclosure:** provider keys and credentials never enter model context or client bundles; prompts contain only job-scoped data; no cross-tenant data in any context assembly (queries all org-scoped). -- **LLM08 Excessive agency:** grants + approval gates + budgets as above; workers cannot self-modify configuration (version immutability; improvement proposals require human approval). -- **LLM09 Overreliance:** deterministic + judge evaluations, acceptance workflow, "Simulated" and confidence labeling; reviews surface degradation instead of silently trusting output. -- **No chain-of-thought exposure:** we store structured decisions and humanized summaries only. +--- -## 6. Application & platform hardening +## 4. Residual risks (honest list) -- **Input validation:** Zod at every boundary — server actions, route handlers, tool args, LLM outputs, env vars (`config.ts` fails fast on invalid env). -- **Security headers** (middleware/`next.config.ts`): CSP (`default-src 'self'`; Next-compatible script/style policy; `frame-ancestors 'none'`; `object-src 'none'`), `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy` minimal, HSTS in production. -- **Rate limiting:** in-memory token buckets per org+IP on login, job scoping, run-now, chat, replacement (the LLM-spend endpoints). Upgrades to Redis/Upstash in Phase 2 for multi-instance. -- **Secrets:** `.env` gitignored; `.env.example` documents every var; no `NEXT_PUBLIC_` secret leakage (lint check); Credential vault rows encrypted AES-256-GCM with `APP_ENCRYPTION_KEY` (32-byte, env-provided), IV+authTag per row; masked rendering only. -- **Downloads:** deliverable download route sets explicit `Content-Type`, `Content-Disposition: attachment`, `X-Content-Type-Options: nosniff`; content served from DB (no user-controlled paths — no traversal surface). -- **Error hygiene:** `AppError` codes → generic client messages; stack traces and internals only in server logs; 404 for both missing and unauthorized. -- **Dependency hygiene:** pinned stack, lockfile committed, `npm audit` (high+) in CI, no post-install scripts from untrusted packages, minimal dependency surface. -- **DB safety:** Prisma parameterized queries only (raw SQL confined to the documented queue claim, parameter-free); least-privilege DB user in production (no DDL at runtime); TLS to managed Postgres in Phase 2. +- **Indirect prompt injection is mitigated, not solved.** Provenance limits *where* data can go, approvals gate *sending*, and tool output is bounded — but a hostile page can still steer a worker's reasoning and poison a deliverable's content. Human review of deliverables remains the real control. +- **No per-tenant provider keys.** All live model spend runs on platform keys, bounded by the monthly budget and the run limits. A tenant that exhausts its budget stops; the platform still carries the cost until billing exists. +- **No self-service password reset.** There is no email provider in this phase. An owner-initiated reset (or a support-assisted one) is the only path; invitations are shared as links by the inviting admin. +- **No 2FA / SSO.** Passwords only. A stolen password gives full workspace access until the session version is bumped. +- **`style-src 'unsafe-inline'`.** Required by React SSR style attributes and the toast library; scripts are nonce-only. +- **Rate limits are per-subject, not per-network.** A distributed attacker with many IPs can still spread sign-in attempts across accounts; the per-account rule is the backstop. +- **Trace payloads contain tenant data** (prompts, fetched page text) until the retention sweep clears them. Access to the database is access to that data. +- **`simulated` mode is the default everywhere.** It is safe by construction, but it also means the live paths get less production mileage; treat the first live deployment as a fresh surface. +- **Deliverable content is not scanned for malware**; it is worker-generated text served inert and never executed. -## 7. Auditability +--- -- Append-only `ActivityEvent` + immutable `ToolCall`/`ModelCall`/`Approval` rows = full who/what/when for every worker action, human approval, and configuration change (version history with `changeSummary`). -- Debug trace view is org-scoped and permission-checked like all other data. +## 5. Reporting a vulnerability -## 8. Production-readiness checklist (Phase 2 exit gate) +Email **security@** the operating organization (or open a private security advisory on the repository) with: what you found, how to reproduce it, what you could access, and whether any tenant data was involved. Please do not open a public issue, do not test against workspaces that are not yours, and do not run denial-of-service tests against shared infrastructure. We aim to acknowledge within two business days and to ship a fix or a mitigation before any public disclosure; we are happy to credit you. -Baseline delivered in Phase 1 (marked ✅ when built), the rest are Phase 2 launch blockers: +--- -- Phase 1: all of sections 2–7 above except where noted "Phase 2"; health endpoint; graceful executor shutdown; seeded-demo credentials documented as demo-only. -- [ ] Hosted TLS + HSTS preload; secrets in platform secret manager (Vercel/Neon), rotated quarterly -- [ ] Managed Postgres with PITR backups + restore drill; connection pooling (pgBouncer) -- [ ] Distributed rate limiting (Upstash); WAF/bot protection at edge; request-id structured logging -- [ ] Error monitoring (Sentry) + uptime alerts + spend-anomaly alerts (daily USD per org vs baseline) -- [ ] Queue moved to Inngest/Trigger.dev with dead-letter handling and idempotency keys -- [ ] 2FA, org invites, session revocation, admin-gated destructive actions -- [ ] Dependency scanning (Dependabot) + CI security gates; `npm audit` clean at high severity -- [ ] Pen-test pass on tenant isolation + SSRF + IDOR (scripted attack suite in CI, external test pre-launch) -- [ ] Data processing inventory + privacy policy + DPA template (design partners); SOC 2 controls mapped (Type I track) -- [ ] Incident response runbook (see OPERATIONS.md): sev levels, key rotation, tenant notification +## 6. Verifying the controls -## 9. Explicit non-claims (honesty ledger) +```bash +npx vitest run tests/security # limiter windows/lockouts/concurrency, CSP, redaction, budget, provenance, routes +npx vitest run tests/auth tests/tools tests/platform/secrets.test.ts +npm run audit:prod # dependency advisories (expected: 0) +curl -sI https:/// | grep -Ei 'content-security-policy|strict-transport|x-frame|x-content-type' +``` -MVP is a locally run demo: credentials auth with a seeded demo user, in-memory rate limits (single process), no TLS on localhost, no formal pen test yet. It is architected so that Phase 2 hardening is configuration and swaps — not redesign. "No one can hack into it" is approached as: minimized attack surface, defense in depth, verified tenant isolation, and a tested path to the production checklist above — never as a marketing absolute. +After a deploy, load one page per route group with the browser console open and confirm there are no CSP violations (charts, dialogs and toasts are the ones worth checking), and confirm `/api/health` and `/api/ready` are reachable without a session while every other route redirects or 401s. diff --git a/eslint.config.mjs b/eslint.config.mjs index 719cea2..ba6d335 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -17,6 +17,8 @@ const eslintConfig = [ ".next/**", "out/**", "build/**", + // Generated by `npm run build:worker` (esbuild bundle) — not source. + "dist/**", "next-env.d.ts", ], }, diff --git a/next.config.ts b/next.config.ts index e9ffa30..2d72f07 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,59 @@ import type { NextConfig } from "next"; +const isProd = process.env.NODE_ENV === "production"; + +/** + * Static security headers for every response. The Content-Security-Policy is NOT here: it needs a per-request + * nonce, so src/middleware.ts sets it (and Next stamps the nonce onto its own inline scripts). + */ +const securityHeaders = [ + // Only meaningful over HTTPS; add `; preload` once every subdomain is confirmed HTTPS-only. + ...(isProd ? [{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" }] : []), + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "X-Frame-Options", value: "DENY" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { + key: "Permissions-Policy", + value: "camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=(), browsing-topics=()", + }, + { key: "Cross-Origin-Opener-Policy", value: "same-origin" }, + { key: "Cross-Origin-Resource-Policy", value: "same-origin" }, + { key: "X-Permitted-Cross-Domain-Policies", value: "none" }, + { key: "X-DNS-Prefetch-Control", value: "off" }, +]; + +const allowedOrigins = process.env.SERVER_ACTIONS_ALLOWED_ORIGINS?.split(",") + .map((s) => s.trim()) + .filter(Boolean); + const nextConfig: NextConfig = { - /* config options here */ + // Self-contained server bundle for the Docker image (node .next/standalone/server.js). + output: "standalone", + poweredByHeader: false, + reactStrictMode: true, + async headers() { + return [{ source: "/:path*", headers: securityHeaders }]; + }, + /** + * Because the app has middleware, Next also compiles `instrumentation.ts` for the Edge runtime. Its + * `import("@/server/runtime")` is guarded by `NEXT_RUNTIME === "nodejs"` and never executes there, but webpack + * still follows it and fails on the Node built-ins the tools layer needs (node:net, node:dns, node:http…). + * Resolving that one specifier to an empty module in the Edge compiler keeps the guard honest and the build green. + * `$` makes it an exact match, so `@/server/runtime/types` (pure types used by client components) still resolves. + */ + webpack(config, { nextRuntime }) { + if (nextRuntime === "edge") { + config.resolve.alias = { ...config.resolve.alias, "@/server/runtime$": false }; + } + return config; + }, + experimental: { + serverActions: { + // Largest legitimate payload is a job description / chat message; keep the ceiling tight. + bodySizeLimit: "256kb", + ...(allowedOrigins && allowedOrigins.length > 0 ? { allowedOrigins } : {}), + }, + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 228eb83..fd54f51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,12 +7,13 @@ "": { "name": "ai-staffing-agency", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { "@ai-sdk/anthropic": "^2.0.102", "@ai-sdk/google": "^2.0.97", "@ai-sdk/openai": "^2.0.127", "@prisma/client": "^6.19.3", - "ai": "^5.0.259", + "ai": "^5.0.262", "bcryptjs": "^3.0.3", "cheerio": "^1.2.0", "class-variance-authority": "^0.7.1", @@ -29,7 +30,6 @@ "react": "19.1.0", "react-dom": "19.1.0", "recharts": "^3.10.1", - "shadcn": "^4.21.0", "sonner": "^2.0.8", "tailwind-merge": "^3.7.0", "tw-animate-css": "^1.4.0", @@ -44,8 +44,10 @@ "@types/react": "^19", "@types/react-dom": "^19", "dotenv": "^17.4.2", + "esbuild": "^0.28.2", "eslint": "^9", "eslint-config-next": "15.5.25", + "shadcn": "^4.21.0", "tailwindcss": "^4", "tsx": "^4.23.13", "typescript": "^5", @@ -69,9 +71,9 @@ } }, "node_modules/@ai-sdk/gateway": { - "version": "2.0.153", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.153.tgz", - "integrity": "sha512-USFJ7MIRxR6+PrXT06Q7V+cJf/rJIOmH/KdY84OWCb6+9bhg4xg1pUNxKD28N4F8+VFnX42wfWtqbM3+ZhK+yQ==", + "version": "2.0.154", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.154.tgz", + "integrity": "sha512-3g7fFx5rcIaaFJaPh0IZY5SZEfmsjfTL8cpGnRRFV/Id7SslepMNBmH3jYZ6AW5suHuvvIDmro3ea/GM1ikxvg==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "2.0.4", @@ -193,6 +195,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", @@ -207,6 +210,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -216,6 +220,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -246,6 +251,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -258,6 +264,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -267,6 +274,7 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.8", @@ -283,6 +291,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.7" @@ -295,6 +304,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.29.7", @@ -311,6 +321,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -320,6 +331,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", @@ -341,6 +353,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -350,6 +363,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -359,6 +373,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.7", @@ -372,6 +387,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.7", @@ -385,6 +401,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.29.7", @@ -402,6 +419,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.7" @@ -414,6 +432,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -423,6 +442,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", @@ -440,6 +460,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.7", @@ -453,6 +474,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -462,6 +484,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -471,6 +494,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -480,6 +504,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.29.7", @@ -493,6 +518,7 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.8" @@ -508,6 +534,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" @@ -523,6 +550,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" @@ -538,6 +566,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.29.7", @@ -554,6 +583,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", @@ -573,6 +603,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", @@ -592,6 +623,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -606,6 +638,7 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -624,6 +657,7 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -637,6 +671,7 @@ "version": "1.75.1", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.75.1.tgz", "integrity": "sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@dotenvx/primitives": "^0.8.0", @@ -667,6 +702,7 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=16" @@ -676,6 +712,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -685,6 +722,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", @@ -708,6 +746,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -725,6 +764,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -737,6 +777,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.17.0" @@ -746,6 +787,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, "license": "MIT", "bin": { "is-docker": "cli.js" @@ -761,6 +803,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -773,6 +816,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, "license": "MIT", "dependencies": { "is-docker": "^2.0.0" @@ -785,6 +829,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -794,6 +839,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.0.0" @@ -806,6 +852,7 @@ "version": "8.4.2", "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, "license": "MIT", "dependencies": { "define-lazy-prop": "^2.0.0", @@ -823,6 +870,7 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -835,12 +883,14 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, "license": "ISC" }, "node_modules/@dotenvx/dotenvx/node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -850,6 +900,7 @@ "version": "7.29.1", "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=20.18.1" @@ -859,6 +910,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -874,6 +926,7 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/@dotenvx/primitives/-/primitives-0.8.0.tgz", "integrity": "sha512-VYJy0uhFm9zTJ1TxBaW/pA8bjbOM/OttaNMwZ1RHG4JKyRG7DhSdiqD1ipQoAyoD22olUtxbP78W9xY3Wd11bg==", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@emnapi/core": { @@ -1495,15 +1548,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/@floating-ui/core": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", @@ -1546,6 +1590,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -2125,6 +2170,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2135,6 +2181,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2145,6 +2192,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2154,12 +2202,14 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2170,6 +2220,7 @@ "version": "1.30.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "dev": true, "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", @@ -2210,6 +2261,7 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -2226,6 +2278,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, "license": "MIT" }, "node_modules/@napi-rs/wasm-runtime": { @@ -2398,6 +2451,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -2411,6 +2465,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -2420,6 +2475,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -4348,12 +4404,14 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, "license": "MIT" }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -4658,6 +4716,7 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==", + "dev": true, "license": "MIT", "dependencies": { "fast-glob": "^3.3.3", @@ -4669,6 +4728,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -4678,6 +4738,7 @@ "version": "5.0.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -4690,6 +4751,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -4706,6 +4768,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -4718,6 +4781,7 @@ "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.8" @@ -4899,6 +4963,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", + "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -5633,6 +5698,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, "license": "MIT", "dependencies": { "mime-types": "^3.0.0", @@ -5666,12 +5732,12 @@ } }, "node_modules/ai": { - "version": "5.0.259", - "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.259.tgz", - "integrity": "sha512-Er9lVNxOmW3dwWg+s6/OZXyNZr36hA+SXbr/5p+OJtVVaNOkXrPEeX0rYWVpyMLsteefXrTcwFYQ0NvRjI3nlA==", + "version": "5.0.262", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.262.tgz", + "integrity": "sha512-9z25r9a8kQBrbPQAk9EWsMVRdNrU66fI/7GKtxe6TrgGF3DxuOg90FVNvqDDMDQ69IFpDFdjUWYUMRIC/nCpWA==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/gateway": "2.0.153", + "@ai-sdk/gateway": "2.0.154", "@ai-sdk/provider": "2.0.4", "@ai-sdk/provider-utils": "3.0.37", "@opentelemetry/api": "1.9.0" @@ -5704,6 +5770,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -5721,6 +5788,7 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -5737,12 +5805,14 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, "license": "MIT" }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5752,6 +5822,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5777,6 +5848,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { @@ -5975,6 +6047,7 @@ "version": "0.16.3", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.3.tgz", "integrity": "sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA==", + "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.1" @@ -6004,6 +6077,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==", + "dev": true, "license": "MIT", "engines": { "node": ">=10.12.0" @@ -6056,6 +6130,7 @@ "version": "2.11.25", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.25.tgz", "integrity": "sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==", + "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -6077,6 +6152,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "^3.1.2", @@ -6101,6 +6177,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -6114,6 +6191,7 @@ "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -6147,6 +6225,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -6159,6 +6238,7 @@ "version": "4.29.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz", "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -6192,6 +6272,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, "license": "MIT", "dependencies": { "run-applescript": "^7.0.0" @@ -6207,6 +6288,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -6275,6 +6357,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6288,6 +6371,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -6304,6 +6388,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6447,6 +6532,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, "license": "MIT", "dependencies": { "restore-cursor": "^5.0.0" @@ -6462,6 +6548,7 @@ "version": "2.9.2", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6501,6 +6588,7 @@ "version": "13.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true, "license": "MIT" }, "node_modules/color-convert": { @@ -6527,6 +6615,7 @@ "version": "14.0.3", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -6543,6 +6632,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz", "integrity": "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.6.3", @@ -6567,6 +6657,7 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -6583,6 +6674,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -6600,12 +6692,14 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, "license": "MIT" }, "node_modules/conf/node_modules/json-schema-typed": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==", + "dev": true, "license": "BSD-2-Clause" }, "node_modules/confbox": { @@ -6627,6 +6721,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -6640,6 +6735,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6649,12 +6745,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, "license": "MIT" }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6664,6 +6762,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.6.0" @@ -6673,6 +6772,7 @@ "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4", @@ -6690,6 +6790,7 @@ "version": "9.0.2", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.1", @@ -6716,6 +6817,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -6758,6 +6860,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -6969,6 +7072,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^3.0.0" @@ -6984,6 +7088,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -7007,6 +7112,7 @@ "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -7028,24 +7134,36 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/deepmerge-ts": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-8.0.2.tgz", + "integrity": "sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==", + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/deepmerge-ts" + } + ], "license": "BSD-3-Clause", "engines": { - "node": ">=16.0.0" + "node": ">=16.9.0" } }, "node_modules/default-browser": { "version": "5.5.1", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "dev": true, "license": "MIT", "dependencies": { "bundle-name": "^4.1.0", @@ -7062,6 +7180,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -7092,6 +7211,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7128,6 +7248,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -7159,6 +7280,7 @@ "version": "8.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -7236,6 +7358,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "dev": true, "license": "MIT", "dependencies": { "is-obj": "^2.0.0" @@ -7251,6 +7374,7 @@ "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -7263,6 +7387,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -7277,6 +7402,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, "license": "MIT" }, "node_modules/effect": { @@ -7293,6 +7419,7 @@ "version": "1.5.430", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.430.tgz", "integrity": "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ==", + "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { @@ -7315,6 +7442,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -7351,6 +7479,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-colors": "^4.1.1", @@ -7376,6 +7505,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7385,6 +7515,7 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -7482,6 +7613,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7491,6 +7623,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7535,6 +7668,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -7651,6 +7785,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7660,6 +7795,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { @@ -8041,6 +8177,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -8110,6 +8247,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -8125,6 +8263,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, "license": "MIT", "dependencies": { "eventsource-parser": "^3.0.1" @@ -8146,6 +8285,7 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, "license": "MIT", "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", @@ -8182,6 +8322,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, "license": "MIT", "dependencies": { "accepts": "^2.0.0", @@ -8225,6 +8366,7 @@ "version": "8.7.0", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.3", @@ -8272,6 +8414,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -8322,6 +8465,7 @@ "version": "3.1.8", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, "funding": [ { "type": "github", @@ -8338,6 +8482,7 @@ "version": "1.20.3", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -8347,6 +8492,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, "license": "MIT", "dependencies": { "is-unicode-supported": "^2.0.0" @@ -8375,6 +8521,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -8387,6 +8534,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -8462,6 +8610,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -8471,6 +8620,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -8480,6 +8630,7 @@ "version": "11.4.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -8509,6 +8660,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8552,6 +8704,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", + "dev": true, "license": "MIT" }, "node_modules/generator-function": { @@ -8568,6 +8721,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -8577,6 +8731,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -8589,6 +8744,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -8622,6 +8778,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz", "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==", + "dev": true, "license": "MIT", "engines": { "node": ">=14.16" @@ -8634,6 +8791,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -8647,6 +8805,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, "license": "MIT", "dependencies": { "@sec-ant/readable-stream": "^0.4.1", @@ -8754,6 +8913,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8766,6 +8926,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/has-bigints": { @@ -8824,6 +8985,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8852,6 +9014,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -8864,6 +9027,7 @@ "version": "4.13.8", "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.8.tgz", "integrity": "sha512-/Gng7NfoykZl2pjukW5Z6+8Yxm3BPRf86GTbQnt0SbySkvax4fyL4H3HhY1cCpBGmiW9XDRFzRV+CXK2W8QudQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=16.9.0" @@ -8904,6 +9068,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, "license": "MIT", "dependencies": { "depd": "~2.0.0", @@ -8924,6 +9089,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18.0" @@ -8945,6 +9111,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -8964,6 +9131,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -8990,6 +9158,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/internal-slot": { @@ -9020,6 +9189,7 @@ "version": "10.7.2", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -9029,6 +9199,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -9056,6 +9227,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, "license": "MIT" }, "node_modules/is-async-function": { @@ -9189,6 +9361,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, "license": "MIT", "bin": { "is-docker": "cli.js" @@ -9220,6 +9393,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9265,6 +9439,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -9277,6 +9452,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -9289,6 +9465,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, "license": "MIT", "dependencies": { "is-docker": "^3.0.0" @@ -9307,6 +9484,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -9345,6 +9523,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -9371,6 +9550,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9380,6 +9560,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -9392,6 +9573,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, "license": "MIT" }, "node_modules/is-regex": { @@ -9417,6 +9599,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -9458,6 +9641,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -9521,6 +9705,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -9579,6 +9764,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" @@ -9601,6 +9787,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -9643,12 +9830,14 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, "funding": [ { "type": "github", @@ -9671,6 +9860,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -9690,6 +9880,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, "license": "MIT" }, "node_modules/json-schema": { @@ -9709,6 +9900,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, "license": "BSD-2-Clause" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -9735,6 +9927,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -9773,6 +9966,7 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10077,6 +10271,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -10106,6 +10301,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, "license": "MIT", "dependencies": { "chalk": "^5.3.0", @@ -10122,6 +10318,7 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -10134,6 +10331,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -10159,6 +10357,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -10187,6 +10386,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10196,6 +10396,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -10209,6 +10410,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -10221,12 +10423,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -10236,6 +10440,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -10249,6 +10454,7 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -10258,6 +10464,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -10274,6 +10481,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10283,6 +10491,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -10308,6 +10517,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10317,6 +10527,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -10364,6 +10575,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "dev": true, "license": "MIT", "dependencies": { "content-type": "^2.1.0" @@ -10380,6 +10592,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -10478,34 +10691,6 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/node-exports-info": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", @@ -10545,6 +10730,7 @@ "version": "2.0.56", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.56.tgz", "integrity": "sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -10554,6 +10740,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^4.0.0", @@ -10570,6 +10757,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -10626,6 +10814,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10635,6 +10824,7 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10657,6 +10847,7 @@ "version": "1.1.33", "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 10" @@ -10776,6 +10967,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -10788,6 +10980,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -10797,6 +10990,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -10812,6 +11006,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10821,6 +11016,7 @@ "version": "11.0.4", "resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz", "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==", + "dev": true, "license": "MIT", "dependencies": { "default-browser": "^5.5.1", @@ -10859,6 +11055,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, "license": "MIT", "dependencies": { "chalk": "^5.3.0", @@ -10882,6 +11079,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -10894,6 +11092,7 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -10906,6 +11105,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.2.2" @@ -10972,6 +11172,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10987,6 +11188,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -10999,6 +11201,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -11017,6 +11220,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -11078,6 +11282,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -11087,6 +11292,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, "license": "MIT" }, "node_modules/path-exists": { @@ -11103,6 +11309,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -11119,6 +11326,7 @@ "version": "8.4.2", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -11147,6 +11355,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -11159,6 +11368,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=16.20.0" @@ -11185,6 +11395,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, "license": "MIT", "dependencies": { "find-up": "^3.0.0" @@ -11197,6 +11408,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^3.0.0" @@ -11209,6 +11421,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^3.0.0", @@ -11222,6 +11435,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -11237,6 +11451,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.0.0" @@ -11249,6 +11464,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -11296,6 +11512,7 @@ "version": "7.1.6", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -11309,6 +11526,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz", "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==", + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -11350,6 +11568,7 @@ "version": "9.3.1", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz", "integrity": "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==", + "dev": true, "license": "MIT", "dependencies": { "parse-ms": "^4.0.0" @@ -11390,6 +11609,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, "license": "MIT", "dependencies": { "kleur": "^3.0.3", @@ -11403,6 +11623,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -11424,6 +11645,7 @@ "version": "2.0.8", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -11467,6 +11689,7 @@ "version": "6.16.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "es-define-property": "^1.0.1", @@ -11483,6 +11706,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -11580,6 +11804,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -11593,6 +11818,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -11608,6 +11834,7 @@ "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -11766,6 +11993,7 @@ "version": "0.23.21", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.21.tgz", "integrity": "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==", + "dev": true, "license": "MIT", "dependencies": { "ast-types": "^0.16.1", @@ -11871,6 +12099,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11910,6 +12139,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -11929,6 +12159,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, "license": "MIT", "dependencies": { "onetime": "^7.0.0", @@ -11945,6 +12176,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, "license": "MIT", "dependencies": { "mimic-function": "^5.0.0" @@ -11960,6 +12192,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -12004,6 +12237,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -12020,6 +12254,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -12032,6 +12267,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -12122,6 +12358,7 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -12134,6 +12371,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.3", @@ -12160,6 +12398,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -12228,12 +12467,14 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, "license": "ISC" }, "node_modules/shadcn": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.21.0.tgz", "integrity": "sha512-UU2mFNusW8C5rvadKdH69vERYZqUlOOlXBcf0MYhYLdTGP6DPti7X4qovCu+RTfCqsAgq/T+YfE0Vnttxh9aiw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.28.0", @@ -12281,6 +12522,7 @@ "version": "0.2.6", "resolved": "https://registry.npmjs.org/cn/-/cn-0.2.6.tgz", "integrity": "sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ==", + "dev": true, "license": "MIT", "bin": { "cn": "bin/cn.mjs" @@ -12293,6 +12535,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -12309,6 +12552,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -12321,6 +12565,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -12333,6 +12578,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, "license": "MIT", "dependencies": { "json5": "^2.2.2", @@ -12347,6 +12593,7 @@ "version": "7.29.1", "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=20.18.1" @@ -12356,6 +12603,7 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -12415,6 +12663,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -12427,6 +12676,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12436,6 +12686,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -12455,6 +12706,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -12471,6 +12723,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -12489,6 +12742,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -12515,6 +12769,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -12527,12 +12782,14 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, "license": "MIT" }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6.0.0", @@ -12543,6 +12800,7 @@ "version": "2.8.10", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "dev": true, "license": "MIT", "dependencies": { "ip-address": "^10.1.1", @@ -12573,6 +12831,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -12605,6 +12864,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -12621,6 +12881,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -12647,6 +12908,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -12664,6 +12926,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -12676,12 +12939,14 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, "license": "MIT" }, "node_modules/string-width/node_modules/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.2.2" @@ -12811,6 +13076,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz", "integrity": "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "get-own-enumerable-keys": "^1.0.0", @@ -12828,6 +13094,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -12840,6 +13107,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -12852,6 +13120,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -12861,6 +13130,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -12935,6 +13205,7 @@ "version": "5.33.11", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.11.tgz", "integrity": "sha512-CNGk+882EQbsbPzaoxeBcYyx+nk0uTj5JzW8w0EXGSCazndNRAh9SE0V5tyMbdONGKilbIBJsyskF4WmZDk7uA==", + "dev": true, "license": "MIT", "os": [ "darwin", @@ -13072,6 +13343,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -13084,6 +13356,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.6" @@ -13106,6 +13379,7 @@ "version": "26.0.0", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==", + "dev": true, "license": "MIT", "dependencies": { "@ts-morph/common": "~0.27.0", @@ -13176,6 +13450,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dev": true, "license": "MIT", "dependencies": { "content-type": "^2.0.0", @@ -13194,6 +13469,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -13314,15 +13590,12 @@ } }, "node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz", + "integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==", "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, "engines": { - "node": ">=14.0" + "node": ">=18.17" } }, "node_modules/undici-types": { @@ -13336,6 +13609,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -13348,6 +13622,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -13357,6 +13632,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -13404,6 +13680,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -13496,12 +13773,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/validate-npm-package-name": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, "license": "ISC", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -13511,6 +13790,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -14019,6 +14299,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -14150,12 +14431,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, "node_modules/wsl-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.0.tgz", "integrity": "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==", + "dev": true, "license": "MIT", "dependencies": { "is-wsl": "^3.1.0", @@ -14172,6 +14455,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -14184,6 +14468,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, "license": "ISC" }, "node_modules/yocto-queue": { @@ -14203,6 +14488,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-1.2.2.tgz", "integrity": "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng==", + "dev": true, "license": "MIT", "dependencies": { "yoctocolors": "^2.1.1" @@ -14218,6 +14504,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -14239,6 +14526,7 @@ "version": "3.25.2", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, "license": "ISC", "peerDependencies": { "zod": "^3.25.28 || ^4" diff --git a/package.json b/package.json index 9812931..e28083e 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,15 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "node scripts/dev.mjs", + "prebuild": "node scripts/generate-migrations-manifest.mjs", "build": "next build", "start": "next start", + "start:web": "node .next/standalone/server.js", + "migrations:manifest": "node scripts/generate-migrations-manifest.mjs", + "build:worker": "esbuild src/worker.ts --bundle --platform=node --target=node22 --format=cjs --packages=external --tsconfig=tsconfig.json --outfile=dist/worker.cjs", + "worker": "tsx src/worker.ts", + "start:worker": "node dist/worker.cjs", "lint": "eslint", "typecheck": "tsc --noEmit", "test": "vitest run", @@ -14,15 +20,19 @@ "db:migrate": "prisma migrate dev", "db:deploy": "prisma migrate deploy", "db:seed": "tsx prisma/seed.ts", + "db:seed:demo": "tsx prisma/seed.ts", "db:reset": "prisma migrate reset --force", - "setup": "prisma migrate deploy && tsx prisma/seed.ts" + "setup": "prisma migrate deploy", + "setup:dev": "prisma migrate deploy && tsx prisma/seed.ts", + "audit:prod": "npm audit --omit=dev --audit-level=high", + "dev:web": "next dev" }, "dependencies": { "@ai-sdk/anthropic": "^2.0.102", "@ai-sdk/google": "^2.0.97", "@ai-sdk/openai": "^2.0.127", "@prisma/client": "^6.19.3", - "ai": "^5.0.259", + "ai": "^5.0.262", "bcryptjs": "^3.0.3", "cheerio": "^1.2.0", "class-variance-authority": "^0.7.1", @@ -39,7 +49,6 @@ "react": "19.1.0", "react-dom": "19.1.0", "recharts": "^3.10.1", - "shadcn": "^4.21.0", "sonner": "^2.0.8", "tailwind-merge": "^3.7.0", "tw-animate-css": "^1.4.0", @@ -54,8 +63,10 @@ "@types/react": "^19", "@types/react-dom": "^19", "dotenv": "^17.4.2", + "esbuild": "^0.28.2", "eslint": "^9", "eslint-config-next": "15.5.25", + "shadcn": "^4.21.0", "tailwindcss": "^4", "tsx": "^4.23.13", "typescript": "^5", @@ -63,5 +74,16 @@ }, "prisma": { "seed": "tsx prisma/seed.ts" + }, + "overrides": { + "@ai-sdk/provider-utils": { + "undici": "^6.28.1" + }, + "next": { + "postcss": "^8.5.28" + }, + "@prisma/config": { + "deepmerge-ts": "^8.0.2" + } } } diff --git a/prisma/migrations/20260922090000_production_security/migration.sql b/prisma/migrations/20260922090000_production_security/migration.sql new file mode 100644 index 0000000..cac5391 --- /dev/null +++ b/prisma/migrations/20260922090000_production_security/migration.sql @@ -0,0 +1,172 @@ +-- CreateEnum +CREATE TYPE "SecurityEventType" AS ENUM ('SIGN_UP', 'SIGN_IN_SUCCEEDED', 'SIGN_IN_FAILED', 'SIGN_IN_THROTTLED', 'ACCOUNT_LOCKED', 'SIGN_OUT', 'SESSIONS_REVOKED', 'PASSWORD_CHANGED', 'PASSWORD_REHASHED', 'CREDENTIAL_SET', 'CREDENTIAL_DELETED', 'INVITE_CREATED', 'INVITE_ACCEPTED', 'INVITE_REVOKED', 'ROLE_CHANGED', 'MEMBER_REMOVED', 'ORG_SETTINGS_CHANGED', 'RATE_LIMITED', 'BUDGET_EXCEEDED'); + +-- AlterTable +ALTER TABLE "Organization" ADD COLUMN "isDemo" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "maxActiveWorkers" INTEGER NOT NULL DEFAULT 10, +ADD COLUMN "maxConcurrentRuns" INTEGER NOT NULL DEFAULT 2, +ADD COLUMN "maxQueuedRuns" INTEGER NOT NULL DEFAULT 20, +ADD COLUMN "monthlyBudgetUsd" DECIMAL(12,2), +ADD COLUMN "suspendedAt" TIMESTAMP(3); + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "disabledAt" TIMESTAMP(3), +ADD COLUMN "emailVerifiedAt" TIMESTAMP(3), +ADD COLUMN "lastSignInAt" TIMESTAMP(3), +ADD COLUMN "passwordChangedAt" TIMESTAMP(3), +ADD COLUMN "sessionVersion" INTEGER NOT NULL DEFAULT 0; + +-- CreateTable +CREATE TABLE "RateLimitBucket" ( + "key" TEXT NOT NULL, + "count" INTEGER NOT NULL DEFAULT 0, + "windowStart" TIMESTAMP(3) NOT NULL, + "lockedUntil" TIMESTAMP(3), + "lockCount" INTEGER NOT NULL DEFAULT 0, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "RateLimitBucket_pkey" PRIMARY KEY ("key") +); + +-- CreateTable +CREATE TABLE "SecurityEvent" ( + "id" TEXT NOT NULL, + "organizationId" TEXT, + "userId" TEXT, + "type" "SecurityEventType" NOT NULL, + "emailHash" TEXT, + "ip" TEXT, + "userAgent" TEXT, + "metadata" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "SecurityEvent_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Invitation" ( + "id" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + "email" TEXT NOT NULL, + "role" "UserRole" NOT NULL DEFAULT 'MEMBER', + "tokenHash" TEXT NOT NULL, + "invitedById" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "acceptedAt" TIMESTAMP(3), + "revokedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Invitation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OrgSpendMonth" ( + "organizationId" TEXT NOT NULL, + "month" TEXT NOT NULL, + "costUsd" DECIMAL(14,6) NOT NULL DEFAULT 0, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "OrgSpendMonth_pkey" PRIMARY KEY ("organizationId","month") +); + +-- CreateTable +CREATE TABLE "ExecutorHeartbeat" ( + "executorId" TEXT NOT NULL, + "hostname" TEXT NOT NULL, + "startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "seenAt" TIMESTAMP(3) NOT NULL, + "inFlight" INTEGER NOT NULL DEFAULT 0, + + CONSTRAINT "ExecutorHeartbeat_pkey" PRIMARY KEY ("executorId") +); + +-- CreateIndex +CREATE INDEX "RateLimitBucket_updatedAt_idx" ON "RateLimitBucket"("updatedAt"); + +-- CreateIndex +CREATE INDEX "SecurityEvent_organizationId_createdAt_idx" ON "SecurityEvent"("organizationId", "createdAt"); + +-- CreateIndex +CREATE INDEX "SecurityEvent_userId_createdAt_idx" ON "SecurityEvent"("userId", "createdAt"); + +-- CreateIndex +CREATE INDEX "SecurityEvent_type_createdAt_idx" ON "SecurityEvent"("type", "createdAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "Invitation_tokenHash_key" ON "Invitation"("tokenHash"); + +-- CreateIndex +CREATE INDEX "Invitation_organizationId_createdAt_idx" ON "Invitation"("organizationId", "createdAt"); + +-- CreateIndex +CREATE INDEX "Invitation_email_idx" ON "Invitation"("email"); + +-- CreateIndex +CREATE INDEX "ExecutorHeartbeat_seenAt_idx" ON "ExecutorHeartbeat"("seenAt"); + +-- CreateIndex +CREATE INDEX "ActivityEvent_runId_idx" ON "ActivityEvent"("runId"); + +-- CreateIndex +CREATE INDEX "Deliverable_organizationId_status_idx" ON "Deliverable"("organizationId", "status"); + +-- CreateIndex +CREATE INDEX "Deliverable_workerVersionId_idx" ON "Deliverable"("workerVersionId"); + +-- CreateIndex +CREATE INDEX "Evaluation_organizationId_createdAt_idx" ON "Evaluation"("organizationId", "createdAt"); + +-- CreateIndex +CREATE INDEX "ModelCall_runStepId_idx" ON "ModelCall"("runStepId"); + +-- CreateIndex +CREATE INDEX "Run_organizationId_status_idx" ON "Run"("organizationId", "status"); + +-- CreateIndex +CREATE INDEX "Run_workerId_status_idx" ON "Run"("workerId", "status"); + +-- CreateIndex +CREATE INDEX "Run_jobId_createdAt_idx" ON "Run"("jobId", "createdAt"); + +-- CreateIndex +CREATE INDEX "ToolCall_workerId_createdAt_idx" ON "ToolCall"("workerId", "createdAt"); + +-- CreateIndex +CREATE INDEX "ToolCall_runStepId_idx" ON "ToolCall"("runStepId"); + +-- CreateIndex +CREATE INDEX "WorkerMessage_organizationId_idx" ON "WorkerMessage"("organizationId"); + +-- CreateIndex +CREATE INDEX "WorkerReview_organizationId_idx" ON "WorkerReview"("organizationId"); + +-- CreateIndex +CREATE INDEX "WorkerReview_workerVersionId_idx" ON "WorkerReview"("workerVersionId"); + +-- CreateIndex +CREATE INDEX "WorkerVersion_jobSpecId_idx" ON "WorkerVersion"("jobSpecId"); + +-- CreateIndex +CREATE INDEX "WorkerVersion_parentVersionId_idx" ON "WorkerVersion"("parentVersionId"); + +-- AddForeignKey +ALTER TABLE "SecurityEvent" ADD CONSTRAINT "SecurityEvent_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SecurityEvent" ADD CONSTRAINT "SecurityEvent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Invitation" ADD CONSTRAINT "Invitation_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Invitation" ADD CONSTRAINT "Invitation_invitedById_fkey" FOREIGN KEY ("invitedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OrgSpendMonth" ADD CONSTRAINT "OrgSpendMonth_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; + + +-- Partial index for the executor's claim query (only QUEUED rows are ever scanned). +CREATE INDEX IF NOT EXISTS "Run_queued_claim_idx" ON "Run" ("availableAt", "createdAt") WHERE "status" = 'QUEUED'; + +-- Mark the pre-existing seeded demo workspace so it is refused outside DEMO_MODE. +UPDATE "Organization" SET "isDemo" = true WHERE "slug" = 'acme-robotics'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 013e201..119c4d0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -202,12 +202,48 @@ enum UsageKind { TOOL } +enum SecurityEventType { + SIGN_UP + SIGN_IN_SUCCEEDED + SIGN_IN_FAILED + SIGN_IN_THROTTLED + ACCOUNT_LOCKED + SIGN_OUT + SESSIONS_REVOKED + PASSWORD_CHANGED + PASSWORD_REHASHED + CREDENTIAL_SET + CREDENTIAL_DELETED + INVITE_CREATED + INVITE_ACCEPTED + INVITE_REVOKED + ROLE_CHANGED + MEMBER_REMOVED + ORG_SETTINGS_CHANGED + RATE_LIMITED + BUDGET_EXCEEDED +} + // ───────────────────────────── Tenancy ───────────────────────────── model Organization { - id String @id @default(cuid()) - name String - slug String @unique + id String @id @default(cuid()) + name String + slug String @unique + /// The seeded demo workspace. Sign-in to it is refused unless DEMO_MODE=true. + isDemo Boolean @default(false) + + /// Hard cap on month-to-date REAL (non-simulated) spend in USD. Null → PLATFORM_DEFAULT_MONTHLY_BUDGET_USD. + monthlyBudgetUsd Decimal? @db.Decimal(12, 2) + /// Executor fairness: max RUNNING runs claimed at once for this org. + maxConcurrentRuns Int @default(2) + /// enqueueRun refuses beyond this many QUEUED + RUNNING + WAITING_FOR_APPROVAL runs. + maxQueuedRuns Int @default(20) + /// hireWorker / hireReplacement refuse beyond this many ACTIVE + PAUSED workers. + maxActiveWorkers Int @default(10) + /// Operator kill switch: when set, no runs are claimed/enqueued and no LLM actions are allowed. + suspendedAt DateTime? + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -224,19 +260,32 @@ model Organization { usageRecords UsageRecord[] credentials Credential[] modelCalls ModelCall[] + securityEvents SecurityEvent[] + invitations Invitation[] + spendMonths OrgSpendMonth[] } model User { - id String @id @default(cuid()) - organizationId String - email String @unique - name String - passwordHash String - role UserRole @default(MEMBER) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + organizationId String + email String @unique + name String + passwordHash String + role UserRole @default(MEMBER) + /// Copied into the JWT at sign-in; bumped on password change / role change / "sign out everywhere". + /// getSession() rejects tokens carrying an older value, which is how sessions are revoked. + sessionVersion Int @default(0) + passwordChangedAt DateTime? + lastSignInAt DateTime? + /// Non-null = account disabled (removed from the workspace); treated as signed out. + disabledAt DateTime? + emailVerifiedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + securityEvents SecurityEvent[] + invitesSent Invitation[] @relation("InvitedBy") @@index([organizationId]) } @@ -244,23 +293,23 @@ model User { // ───────────────────────────── Jobs ───────────────────────────── model Job { - id String @id @default(cuid()) - organizationId String - title String + id String @id @default(cuid()) + organizationId String + title String /// Raw plain-English description the customer typed at intake. - description String + description String /// Job family slug, e.g. "market_research" (see src/server/domain/job-family.ts). - jobFamily String @default("general") - status JobStatus @default(DRAFT) + jobFamily String @default("general") + status JobStatus @default(DRAFT) /// Follow-up Q&A captured during scoping: IntakeAnswers (see domain/job-spec.ts). - intake Json? + intake Json? /// Staffing Engine proposal awaiting "Hire": WorkerProposal (see domain/blueprint.ts). Cleared on hire. pendingProposal Json? - createdById String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdById String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) specs JobSpec[] workers Worker[] runs Run[] @@ -370,6 +419,8 @@ model WorkerVersion { @@unique([workerId, version]) @@index([workerId, status]) + @@index([jobSpecId]) + @@index([parentVersionId]) } /// Server-side permission grant. A tool call is allowed only if the tool is listed in the version @@ -446,6 +497,9 @@ model Run { @@index([organizationId, createdAt]) @@index([workerId, createdAt]) @@index([workerVersionId]) + @@index([organizationId, status]) + @@index([workerId, status]) + @@index([jobId, createdAt]) } model RunStep { @@ -505,6 +559,7 @@ model ModelCall { @@index([runId]) @@index([organizationId, createdAt]) @@index([workerId, createdAt]) + @@index([runStepId]) } model ToolCall { @@ -533,6 +588,8 @@ model ToolCall { @@index([runId]) @@index([workerId, toolName]) + @@index([workerId, createdAt]) + @@index([runStepId]) } // ───────────────────────────── Outputs & quality ───────────────────────────── @@ -568,6 +625,8 @@ model Deliverable { @@index([workerId, createdAt]) @@index([jobId, createdAt]) @@index([runId]) + @@index([organizationId, status]) + @@index([workerVersionId]) } model Evaluation { @@ -598,6 +657,7 @@ model Evaluation { @@index([workerId, createdAt]) @@index([workerVersionId, type]) @@index([runId]) + @@index([organizationId, createdAt]) } model WorkerReview { @@ -624,6 +684,8 @@ model WorkerReview { workerVersion WorkerVersion @relation(fields: [workerVersionId], references: [id], onDelete: Cascade) @@index([workerId, createdAt]) + @@index([organizationId]) + @@index([workerVersionId]) } model Approval { @@ -677,6 +739,7 @@ model WorkerMessage { @@index([workerId, createdAt]) @@index([workerId, classification, instructionActive]) + @@index([organizationId]) } model ActivityEvent { @@ -701,6 +764,7 @@ model ActivityEvent { @@index([organizationId, createdAt]) @@index([workerId, createdAt]) @@index([jobId, createdAt]) + @@index([runId]) } /// Metering ledger. One row per billable model/tool call. Powers the Usage pages now and @@ -749,3 +813,85 @@ model Credential { @@unique([organizationId, name]) } + +// ───────────────────────────── Security & accounts ───────────────────────────── + +/// Fixed-window counters + lockouts shared by every instance (sign-in, sign-up, LLM actions, run enqueue …). +/// key = ":". Rows are disposable; the worker sweeps stale ones. +model RateLimitBucket { + key String @id + count Int @default(0) + windowStart DateTime + lockedUntil DateTime? + /// Consecutive lockouts, for exponential backoff (reset on success). + lockCount Int @default(0) + updatedAt DateTime @updatedAt + + @@index([updatedAt]) +} + +/// Append-only audit log of security-relevant events. Never stores raw passwords, tokens or secret values. +model SecurityEvent { + id String @id @default(cuid()) + organizationId String? + userId String? + type SecurityEventType + /// sha256(normalized email) for attempts against unknown accounts — never the raw address. + emailHash String? + ip String? + /// Clipped to 256 chars. + userAgent String? + metadata Json? + createdAt DateTime @default(now()) + + organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + + @@index([organizationId, createdAt]) + @@index([userId, createdAt]) + @@index([type, createdAt]) +} + +/// Team invitations. The raw token only ever exists in the invite link; we store sha256(token). +model Invitation { + id String @id @default(cuid()) + organizationId String + email String + role UserRole @default(MEMBER) + tokenHash String @unique + invitedById String + expiresAt DateTime + acceptedAt DateTime? + revokedAt DateTime? + createdAt DateTime @default(now()) + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + invitedBy User @relation("InvitedBy", fields: [invitedById], references: [id], onDelete: Cascade) + + @@index([organizationId, createdAt]) + @@index([email]) +} + +/// O(1) month-to-date REAL (non-simulated) spend per org, incremented atomically by usage.recordUsage. +model OrgSpendMonth { + organizationId String + /// UTC "YYYY-MM" + month String + costUsd Decimal @default(0) @db.Decimal(14, 6) + updatedAt DateTime @updatedAt + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + + @@id([organizationId, month]) +} + +/// Worker-process liveness, upserted by each executor every poll tick; read by /api/ready and ops tooling. +model ExecutorHeartbeat { + executorId String @id + hostname String + startedAt DateTime @default(now()) + seenAt DateTime @updatedAt + inFlight Int @default(0) + + @@index([seenAt]) +} diff --git a/prisma/seed.ts b/prisma/seed.ts index 6cb645a..342738e 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,15 +1,28 @@ import "dotenv/config"; import { db } from "@/server/db"; import { seedDemo } from "./seed/demo"; +import { demoSeedRefusalReason } from "./seed/guard"; /** - * `npm run db:seed` — rebuilds the Acme Robotics demo workspace (idempotent; see prisma/seed/demo.ts). + * `npm run db:seed:demo` — rebuilds the Acme Robotics demo workspace (idempotent; see prisma/seed/demo.ts). * Only runs when executed directly, so tests can import `seedDemo` without side effects. + * + * Deployments run `npm run setup` (migrations only). The demo seed is never part of a release path: it writes a + * workspace whose password is in the docs, so it refuses to touch a production database unless ALLOW_DEMO_SEED is + * explicitly set (audit OPS-14). */ export { seedDemo } from "./seed/demo"; +export { demoSeedRefusalReason } from "./seed/guard"; async function main(): Promise { + const refusal = demoSeedRefusalReason(); + if (refusal) { + console.error(refusal); + process.exitCode = 1; + return; + } + const started = Date.now(); const verbose = process.argv.includes("--verbose"); const summary = await seedDemo(db, { log: verbose ? (line) => console.log(line) : undefined }); diff --git a/prisma/seed/demo.ts b/prisma/seed/demo.ts index e41db0f..f90143f 100644 --- a/prisma/seed/demo.ts +++ b/prisma/seed/demo.ts @@ -66,7 +66,9 @@ async function build(db: PrismaClient, now: Date, log: (line: string) => void): await reset(db); const createdAt = new Date(now.getTime() - 22 * DAY_MS); const organization = await db.organization.create({ - data: { id: DEMO_IDS.organizationId, name: DEMO_USER.organizationName, slug: DEMO_USER.organizationSlug, createdAt, updatedAt: createdAt }, + // isDemo marks this workspace as seeded sample data: the UI badges it and operators can tell it apart from a + // real tenant in any deployment that chose to run the demo seed. + data: { id: DEMO_IDS.organizationId, name: DEMO_USER.organizationName, slug: DEMO_USER.organizationSlug, isDemo: true, createdAt, updatedAt: createdAt }, }); const user = await db.user.create({ data: { diff --git a/prisma/seed/guard.ts b/prisma/seed/guard.ts new file mode 100644 index 0000000..70fb55f --- /dev/null +++ b/prisma/seed/guard.ts @@ -0,0 +1,18 @@ +import { config } from "@/server/config"; + +/** + * The demo seed rebuilds a workspace with a publicly documented password (see `DEMO_USER`). That is exactly what + * a demo needs and exactly what a production database must never contain, so the CLI refuses to run there unless + * the operator has deliberately opted in with `ALLOW_DEMO_SEED=true` — the same flag that decides whether the + * sign-in page offers the demo credentials. + * + * `seedDemo()` itself is unguarded: tests import it directly and run with `NODE_ENV=test`. + */ +export function demoSeedRefusalReason(): string | null { + if (config.auth.allowDemoSeed) return null; + return ( + "Refusing to seed demo data: NODE_ENV=production and ALLOW_DEMO_SEED is not set.\n" + + "The demo workspace ships a publicly known password, so it must never be created in a real deployment.\n" + + "If this really is a demo deployment, re-run with ALLOW_DEMO_SEED=true." + ); +} diff --git a/scripts/dev.mjs b/scripts/dev.mjs new file mode 100644 index 0000000..81dd10b --- /dev/null +++ b/scripts/dev.mjs @@ -0,0 +1,59 @@ +#!/usr/bin/env node +/** + * Local development runner: starts the Next.js web server AND the run worker, the same two processes that run + * in production. Runs are executed by the worker, never inside the web server — that keeps the web bundle free + * of Node-only modules (the Edge runtime compiles instrumentation.ts too) and makes local behaviour match a + * real deployment. + * + * npm run dev → web + worker + * EXECUTOR_MODE=off npm run dev → web only (no runs are executed) + * npm run dev:web / npm run worker → either half on its own + */ +import { spawn } from "node:child_process"; + +const withWorker = process.env.EXECUTOR_MODE !== "off" && process.env.EXECUTOR_DISABLED !== "true"; +const children = []; +let shuttingDown = false; + +function start(name, command, args, color) { + const child = spawn(command, args, { stdio: ["inherit", "pipe", "pipe"], env: process.env }); + const tag = `\x1b[${color}m[${name}]\x1b[0m `; + const pipe = (stream, out) => { + let buffer = ""; + stream.on("data", (chunk) => { + buffer += chunk.toString(); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + for (const line of lines) out.write(line.length > 0 ? `${tag}${line}\n` : "\n"); + }); + }; + pipe(child.stdout, process.stdout); + pipe(child.stderr, process.stderr); + child.on("exit", (code, signal) => { + if (shuttingDown) return; + process.stdout.write(`${tag}exited (${signal ?? code}) — stopping everything\n`); + shutdown(typeof code === "number" ? code : 1); + }); + children.push(child); + return child; +} + +function shutdown(code) { + if (shuttingDown) return; + shuttingDown = true; + for (const child of children) child.kill("SIGTERM"); + // Give the worker its graceful-shutdown window before forcing the issue. + setTimeout(() => { + for (const child of children) child.kill("SIGKILL"); + process.exit(code); + }, 5000).unref(); + let remaining = children.length; + for (const child of children) child.on("exit", () => --remaining === 0 && process.exit(code)); +} + +process.on("SIGINT", () => shutdown(0)); +process.on("SIGTERM", () => shutdown(0)); + +start("web", "npx", ["next", "dev", ...process.argv.slice(2)], "36"); +if (withWorker) start("worker", "npx", ["tsx", "src/worker.ts"], "35"); +else process.stdout.write("\x1b[90m[dev] worker disabled (EXECUTOR_MODE=off) — queued runs will not execute\x1b[0m\n"); diff --git a/scripts/generate-migrations-manifest.mjs b/scripts/generate-migrations-manifest.mjs new file mode 100644 index 0000000..b336643 --- /dev/null +++ b/scripts/generate-migrations-manifest.mjs @@ -0,0 +1,37 @@ +#!/usr/bin/env node +/** + * Bakes the list of migration directory names into `src/generated/migrations.json` at build time (npm `prebuild`). + * + * `/api/ready` compares this list against `_prisma_migrations` in the database, so a container that boots against + * a database the release job has not migrated yet reports "not ready" instead of serving requests that will fail + * on a missing column. The file is committed so `tsc`, tests and `next dev` work without running a build first. + */ +import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const migrationsDir = join(root, "prisma", "migrations"); +const outFile = join(root, "src", "generated", "migrations.json"); + +const migrations = readdirSync(migrationsDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + +const next = `${JSON.stringify({ migrations }, null, 2)}\n`; + +let current = ""; +try { + current = readFileSync(outFile, "utf8"); +} catch { + // First run: the file does not exist yet. +} + +if (current === next) { + console.log(`migrations manifest up to date (${migrations.length} migrations)`); +} else { + mkdirSync(dirname(outFile), { recursive: true }); + writeFileSync(outFile, next); + console.log(`wrote src/generated/migrations.json (${migrations.length} migrations)`); +} diff --git a/src/app/(app)/_lib/action-guards.ts b/src/app/(app)/_lib/action-guards.ts new file mode 100644 index 0000000..7210aca --- /dev/null +++ b/src/app/(app)/_lib/action-guards.ts @@ -0,0 +1,81 @@ +import { z } from "zod"; +import type { SessionContext } from "@/server/auth/types"; +import { invalid } from "@/server/errors"; +import { enforce, RATE_RULES } from "@/server/security"; + +/** + * Shared input guards for every server action under `(app)` — the outer edge of the server, where a hostile + * client is the normal case (audit F-011 / INF-15). + * + * Two jobs: + * 1. bound every value before it reaches Prisma, an LLM prompt or `revalidatePath` — ids are opaque strings, + * not paths, and free text has a ceiling so nobody can push a megabyte into a stored spec; + * 2. apply the per-user / per-org rate limits from `@/server/security` to the actions that cost money. + * + * Not a route (Next ignores `_`-prefixed folders); imported only by `"use server"` files. + */ + +/** + * A database id as it travels from the client: cuid-ish. Deliberately not a strict cuid regex — the demo seed + * writes fixed ids like `worker_demo_alex` — but always opaque: no slashes, dots or spaces, so an id can never + * become a path segment in `revalidatePath`. + */ +export const IdSchema = z + .string() + .trim() + .min(1, "Missing id") + .max(64, "That id is not valid") + .regex(/^[A-Za-z0-9_-]+$/, "That id is not valid"); + +/** Parses an id argument, naming what it was ("Worker", "Run") so the toast is human. */ +export function parseId(value: unknown, what: string): string { + const parsed = IdSchema.safeParse(value); + if (!parsed.success) throw invalid(`${what} not found`); + return parsed.data; +} + +/** Same, for the optional `context` ids actions use only to revalidate extra pages. */ +export function parseOptionalId(value: unknown): string | undefined { + if (value === undefined || value === null || value === "") return undefined; + const parsed = IdSchema.safeParse(value); + return parsed.success ? parsed.data : undefined; +} + +export const DECISION_NOTE_MAX = 1_000; +export const FEEDBACK_MAX = 4_000; +export const TOOL_NAME_MAX = 64; + +export const DecisionSchema = z.enum(["approve", "reject"]); +export const ReviewDecisionSchema = z.enum(["accept", "reject"]); +export const ToolNameSchema = z.string().trim().min(1, "Missing tool").max(TOOL_NAME_MAX, "That tool is not valid"); + +/** Optional free text with a hard ceiling; blank becomes undefined so "no note" is never stored as "". */ +const boundedText = (max: number, message: string) => + z + .string() + .trim() + .max(max, message) + .optional() + .transform((value) => (value ? value : undefined)); + +export const NoteSchema = boundedText(DECISION_NOTE_MAX, `Keep the note under ${DECISION_NOTE_MAX.toLocaleString("en-US")} characters`); +export const FeedbackSchema = boundedText(FEEDBACK_MAX, `Keep the feedback under ${FEEDBACK_MAX.toLocaleString("en-US")} characters`); + +/** + * Every action that reaches a model: per user (the human clicking) and per org (the bill). Both fail closed with + * LIMIT_EXCEEDED, which `runAction` turns into a toast. + */ +export async function limitLlmAction(s: SessionContext): Promise { + await enforce(RATE_RULES.llmUser, s.userId); + await enforce(RATE_RULES.llmOrg, s.organizationId); +} + +/** Queueing work: "Run now", retry, and hires that start a first run. */ +export async function limitRunAction(s: SessionContext): Promise { + await enforce(RATE_RULES.runOrg, s.organizationId); +} + +/** Writing to the credential vault (an AES encrypt + a DB write each time). */ +export async function limitCredentialsAction(s: SessionContext): Promise { + await enforce(RATE_RULES.credentialsOrg, s.organizationId); +} diff --git a/src/app/(app)/activity/_components/activity-filters.tsx b/src/app/(app)/activity/_components/activity-filters.tsx index 956f962..05621d7 100644 --- a/src/app/(app)/activity/_components/activity-filters.tsx +++ b/src/app/(app)/activity/_components/activity-filters.tsx @@ -2,86 +2,93 @@ import { useTransition } from "react"; import Link from "next/link"; -import { usePathname, useRouter } from "next/navigation"; -import { Loader2, X } from "lucide-react"; -import { Button } from "@/components/ui/button"; +import { useRouter } from "next/navigation"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { WorkerAvatar } from "@/components/worker-avatar"; +import { cn } from "@/lib/utils"; /** Radix Select cannot represent "no value" as an item, so "all" is a sentinel that maps to no query param. */ const ALL = "all"; export interface ActivityFiltersProps { workers: Array<{ id: string; name: string; title: string; avatarColor: string; status: string }>; + /** Event-type segments, in order. `value` is the `type` query param; "" is the All segment. */ groups: Array<{ value: string; label: string }>; - /** Current selection (from searchParams); undefined = all. */ + /** Current selection (from searchParams); undefined = everything. */ workerId?: string; group?: string; } -/** Worker + event-type filters for the feed. Selections live in the URL so pages are shareable and "Load more" keeps them. */ +function hrefFor(worker: string | undefined, type: string | undefined): string { + const params = new URLSearchParams(); + if (worker) params.set("worker", worker); + if (type) params.set("type", type); + // A new filter always starts from the newest events — an old cursor would make the first page look empty. + const query = params.toString(); + return query ? `/activity?${query}` : "/activity"; +} + +/** + * Event type as a segmented control, worker as a gray pill select. Both live in the URL, so a filtered feed is + * shareable and "Load more" keeps the selection. + */ export function ActivityFilters({ workers, groups, workerId, group }: ActivityFiltersProps) { const router = useRouter(); - const pathname = usePathname(); const [pending, startTransition] = useTransition(); - function apply(next: { worker?: string; type?: string }) { - const params = new URLSearchParams(); - const worker = next.worker ?? workerId; - const type = next.type ?? group; - if (worker && worker !== ALL) params.set("worker", worker); - if (type && type !== ALL) params.set("type", type); - // A new filter always starts from the newest events — an old cursor would make the first page look empty. - const query = params.toString(); - startTransition(() => router.push(query ? `${pathname}?${query}` : pathname)); - } - - const filtered = Boolean(workerId || group); - // An id that matches no worker (stale link) would leave the trigger blank; show "All workers" but keep Clear visible. + // An id that matches no worker (a stale link) would leave the trigger blank; fall back to "Everyone". const workerValue = workerId && workers.some((w) => w.id === workerId) ? workerId : ALL; return ( -
- startTransition(() => router.push(hrefFor(value === ALL ? undefined : value, group)))} + > + + - All workers + Everyone {workers.map((w) => ( - + {w.name} - {w.status === "RETIRED" ? (retired) : null} + {w.status === "RETIRED" ? · retired : null} ))} - - - - {filtered ? ( - - ) : null} - {pending ? : null}
); } diff --git a/src/app/(app)/activity/_components/activity-row.tsx b/src/app/(app)/activity/_components/activity-row.tsx index 42f29af..cfd8dc5 100644 --- a/src/app/(app)/activity/_components/activity-row.tsx +++ b/src/app/(app)/activity/_components/activity-row.tsx @@ -1,88 +1,75 @@ import Link from "next/link"; -import { Bot, ChevronRight, UserRound, type LucideIcon } from "lucide-react"; import { RelativeTime } from "@/components/relative-time"; +import { LogoGlyph } from "@/components/shell/logo"; import { WorkerAvatar } from "@/components/worker-avatar"; +import { initialsOf } from "@/lib/initials"; import { cn } from "@/lib/utils"; import type { ActivityItem } from "@/server/activity"; -const ACTOR_ICON: Record = { USER: UserRound, SYSTEM: Bot, WORKER: Bot }; - -/** Failures and rejections get a rose dot so a bad day is visible at a glance; approvals waiting get amber. */ -function toneDot(type: ActivityItem["type"]): string | null { - switch (type) { - case "RUN_FAILED": - case "DELIVERABLE_REJECTED": - case "APPROVAL_REJECTED": - return "bg-rose-500"; - case "APPROVAL_REQUESTED": - return "bg-amber-500"; - case "RUN_SUCCEEDED": - case "DELIVERABLE_ACCEPTED": - case "WORKER_HIRED": - return "bg-emerald-500"; - default: - return null; +/** + * Who did it. A worker gets their face; a teammate gets their initials on a neutral circle; the platform gets + * the wordmark glyph. No per-type icon — the sentence already says what happened. + */ +function Actor({ item }: { item: ActivityItem }) { + if (item.worker) { + return ; } + const shell = "flex size-8 shrink-0 items-center justify-center rounded-full bg-secondary select-none"; + if (item.actorType === "USER") { + return ( + + {initialsOf(item.actorName)} + + ); + } + return ( + + + + ); } export interface ActivityRowProps { item: ActivityItem; - /** Dense = the Workforce "Recent activity" list. */ + /** Dense = the "Recent activity" card on Workforce: one line per event, no by-line. */ dense?: boolean; } /** - * One feed entry: who did it (worker avatar, or a person/system icon), what happened, and when. The whole row - * links to the most specific page the event knows about (deliverable → approval → version → run → worker). + * One feed entry: who did it, what happened in a sentence, and when. The whole row links to the most specific + * page the event knows about (deliverable → approval → version → run → worker). */ export function ActivityRow({ item, dense = false }: ActivityRowProps) { - const ActorIcon = ACTOR_ICON[item.actorType]; - const dot = toneDot(item.type); + // The sentence already names whoever acted ("Dana approved Maya's request"), so there is no by-line. const body = ( <> -
- {item.worker ? ( - - ) : ( - - - )} - {dot ? ( -
+
-

{item.title}

+

{item.title}

{item.detail ? ( -

{item.detail}

- ) : null} - {!dense && item.actorType === "USER" && item.actorName ? ( -

by {item.actorName}

+

+ {item.detail} +

) : null}
-
+ - {item.href ? ( -
+ ); - const rowClass = cn("group/row flex items-start gap-3", dense ? "py-2.5" : "py-3"); - if (item.href) { - return ( - - {body} - - ); - } - return
{body}
; + const rowClass = cn("flex items-start gap-3.5", dense ? "py-3" : "min-h-14 py-3.5"); + if (!item.href) return
{body}
; + + return ( + + {body} + + ); } diff --git a/src/app/(app)/activity/loading.tsx b/src/app/(app)/activity/loading.tsx index 35f51d0..d7de896 100644 --- a/src/app/(app)/activity/loading.tsx +++ b/src/app/(app)/activity/loading.tsx @@ -1,46 +1,43 @@ -import type { ComponentProps } from "react"; import { Skeleton } from "@/components/ui/skeleton"; -import { cn } from "@/lib/utils"; -function Bone({ className, ...props }: ComponentProps) { - return ; -} - -/** Mirrors /activity: header with the two filter selects → day headings → cards of feed rows. */ +/** Mirrors /activity: title → filter pills → one card per day of feed rows. Never a spinner. */ export default function ActivityLoading() { return (
Loading activity… -
-
- - -
-
- - -
+
+ +
- {[4, 3].map((rows, day) => ( -
- -
- {Array.from({ length: rows }, (_, i) => ( -
- -
- - +
+ + +
+ +
+ {[5, 3].map((rows, day) => ( +
+
+ +
+
+ {Array.from({ length: rows }, (_, i) => ( +
+ +
+ + +
+
- -
- ))} + ))} +
-
- ))} + ))} +
); diff --git a/src/app/(app)/activity/page.tsx b/src/app/(app)/activity/page.tsx index 07319ab..889ba52 100644 --- a/src/app/(app)/activity/page.tsx +++ b/src/app/(app)/activity/page.tsx @@ -1,12 +1,12 @@ import type { Metadata } from "next"; import Link from "next/link"; import { isToday, isYesterday } from "date-fns"; -import { Activity, ArrowUp, ChevronDown } from "lucide-react"; import { AutoRefresh } from "@/components/auto-refresh"; import { EmptyState } from "@/components/empty-state"; +import { LiveDot } from "@/components/live-dot"; import { PageHeader } from "@/components/page-header"; import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; +import { Card } from "@/components/ui/card"; import { formatDate, formatDateTime } from "@/lib/format"; import { requireSession } from "@/server/auth"; import { @@ -18,6 +18,7 @@ import { isActivityGroup, listActivityWorkers, type ActivityDayGroup, + type ActivityGroup, } from "@/server/queries/activity"; import { ActivityFilters } from "./_components/activity-filters"; import { ActivityRow } from "./_components/activity-row"; @@ -28,12 +29,33 @@ interface ActivitySearchParams { worker?: string; type?: string; before?: string; + show?: string; } -const GROUP_OPTIONS = (Object.keys(ACTIVITY_GROUPS) as Array).map((value) => ({ - value, - label: ACTIVITY_GROUP_LABELS[value], -})); +/** "Load more" grows the page in place. Capped so the feed query can still tell us whether older events exist. */ +const PAGE_SIZE = 40; +const MAX_SHOW = 160; + +/** Shorter, friendlier labels than the query module's own — the segments have to fit a phone. */ +const SEGMENT_LABELS: Partial> = { + approvals: "Decisions", + hiring: "Hiring", + permissions: "Permissions", +}; + +const SEGMENTS = [ + { value: "", label: "All" }, + ...(Object.keys(ACTIVITY_GROUPS) as ActivityGroup[]).map((value) => ({ + value, + label: SEGMENT_LABELS[value] ?? ACTIVITY_GROUP_LABELS[value], + })), +]; + +function parseShow(value: string | undefined): number { + const n = Number(value); + if (!Number.isFinite(n)) return PAGE_SIZE; + return Math.min(MAX_SHOW, Math.max(PAGE_SIZE, Math.round(n / PAGE_SIZE) * PAGE_SIZE)); +} function dayLabel(group: ActivityDayGroup): string { // Label from the first event's timestamp (not the yyyy-MM-dd key) so "Today" agrees with the server's clock. @@ -49,6 +71,7 @@ function buildQuery(params: ActivitySearchParams, overrides: Partial - } + actions={live ? : undefined} />
+ + {before ? ( -
- Showing events from before {formatDateTime(before)}. - -
+

) : null} {days.length === 0 ? ( - - Show everything - - ) : ( - - ) - } - /> + + + Show everything + + ) : ( + + ) + } + /> + ) : ( - days.map((day) => ( -
-

{dayLabel(day)}

- - +
+ {days.map((day) => ( + +

+ {dayLabel(day)} +

+
{day.items.map((item) => ( ))} - +
-
- )) + ))} +
)} - {feed.nextCursor ? ( + {more ? (
-
) : days.length > 0 ? ( -

That's everything — you've reached the beginning.

+

That's the beginning of the record.

) : null} {/* Only the live (first) page follows runs in flight; an older page is a snapshot the reader is studying. */} - + ); } diff --git a/src/app/(app)/approvals/_components/approval-card.tsx b/src/app/(app)/approvals/_components/approval-card.tsx index ecd2340..428978e 100644 --- a/src/app/(app)/approvals/_components/approval-card.tsx +++ b/src/app/(app)/approvals/_components/approval-card.tsx @@ -1,61 +1,68 @@ import Link from "next/link"; -import { ArrowUpRight, Wrench } from "lucide-react"; -import { JsonView } from "@/components/json-view"; +import { ChevronRight } from "lucide-react"; import { RelativeTime } from "@/components/relative-time"; import { SimulatedBadge } from "@/components/simulated-badge"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"; import { WorkerAvatar } from "@/components/worker-avatar"; import type { ApprovalView } from "@/server/queries/approvals"; import { ApprovalDecision } from "./approval-decision"; +import { PayloadPreview } from "./payload-preview"; +import { requestSentence } from "./request"; -/** One pending request: who is asking, what exactly they will do (the payload), and the decision controls. */ -export function ApprovalCard({ approval }: { approval: ApprovalView }) { +export interface ApprovalCardProps { + approval: ApprovalView; + /** Why this viewer can't decide, if they can't. The server enforces it either way. */ + disabledReason?: string; +} + +/** One waiting request: who is asking, what exactly they will do, and the two ways to answer. */ +export function ApprovalCard({ approval, disabledReason }: ApprovalCardProps) { const { worker } = approval; + return ( -
- +
+
-

{approval.title}

- {approval.description ? ( -

{approval.description}

- ) : null} -
- - {worker.name} - - - {worker.title} - +

{requestSentence(worker.name, approval.title)}

+

- asked + Requested -

-
-
- - - {approval.simulated ? : null} + + {approval.jobTitle} + {approval.simulated ? ( + <> + + + + ) : null} +

+ - + - - - + ); diff --git a/src/app/(app)/approvals/_components/approval-decision.tsx b/src/app/(app)/approvals/_components/approval-decision.tsx index 437ff24..90869c3 100644 --- a/src/app/(app)/approvals/_components/approval-decision.tsx +++ b/src/app/(app)/approvals/_components/approval-decision.tsx @@ -2,15 +2,14 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; -import { Check, X } from "lucide-react"; import { toast } from "sonner"; import { ConfirmDialog } from "@/components/confirm-dialog"; -import { JsonView } from "@/components/json-view"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { decideApprovalAction } from "../actions"; import { DECISION_NOTE_MAX, type Decision } from "../schema"; +import { PayloadPreview } from "./payload-preview"; export interface ApprovalDecisionProps { approvalId: string; @@ -18,33 +17,53 @@ export interface ApprovalDecisionProps { workerName: string; /** Registry display name of the tool: "Notifications". */ toolLabel: string; - /** Compact = the inline pair on the Workforce attention strip. */ + /** Compact = the inline pair on the Workforce "Needs you" band. */ size?: "default" | "sm"; /** - * The exact tool input awaiting approval. Pass it from surfaces that do NOT already render the payload next to - * the buttons (the Workforce attention strip): the Approve confirmation then shows it, so nobody approves an - * external send without seeing its recipients and full body. + * The exact tool input awaiting approval. Every surface should pass it: the confirmation then shows the + * recipients, subject and message, so nobody approves a send they haven't read. */ payload?: unknown; + /** + * The page's one blue pill belongs to the decision only where deciding *is* the page (/approvals). Elsewhere + * both pills are gray. + */ + emphasis?: "primary" | "quiet"; + /** Set when the viewer's role may not decide this request: the controls go flat and say why. */ + disabledReason?: string; } -const COPY: Record string; body: (name: string, tool: string) => string; confirm: string; success: (name: string) => string }> = { +const COPY: Record string; body: (name: string, tool: string) => string; confirm: string; success: (name: string) => string; label: string; noteLabel: string; placeholder: string }> = { approve: { title: (name) => `Let ${name} go ahead?`, - body: (name, tool) => `${name} will use ${tool} exactly as shown and then carry on with the run.`, + body: (name, tool) => `${name} will use ${tool} exactly as shown, then carry on with the run.`, confirm: "Approve", success: (name) => `Approved — ${name} is picking up where they left off.`, + label: "Approve", + noteLabel: "Note", + placeholder: "Looks good — send it.", }, reject: { title: (name) => `Tell ${name} not to do this?`, - body: (name) => `${name} will skip this action and finish the run without it. A short note helps them adapt.`, - confirm: "Reject", - success: (name) => `Rejected — ${name} will finish the run without it.`, + body: (name) => `${name} will skip this action and finish the run without it. A short reason helps them adapt.`, + confirm: "Decline", + success: (name) => `Declined — ${name} will finish the run without it.`, + label: "Decline", + noteLabel: "Reason", + placeholder: "Not this week — the numbers need a second look.", }, }; -/** Approve / Reject pair. Each opens a confirmation with an optional note that lands on the approval record. */ -export function ApprovalDecision({ approvalId, workerName, toolLabel, size = "default", payload }: ApprovalDecisionProps) { +/** Approve / Decline pair. Each opens a confirmation carrying the full request and an optional note. */ +export function ApprovalDecision({ + approvalId, + workerName, + toolLabel, + size = "default", + payload, + emphasis = "primary", + disabledReason, +}: ApprovalDecisionProps) { const router = useRouter(); const [note, setNote] = useState(""); @@ -57,31 +76,43 @@ export function ApprovalDecision({ approvalId, workerName, toolLabel, size = "de router.refresh(); } + // 44px touch targets on a phone: the h-9 pills stretch to the card's width there. + const pill = size === "default" ? "max-sm:h-11 max-sm:flex-1" : undefined; + const row = size === "default" ? "flex shrink-0 items-center gap-2 max-sm:w-full" : "flex shrink-0 items-center gap-2"; + + if (disabledReason) { + return ( +
+
+ + +
+

{disabledReason}

+
+ ); + } + function description(decision: Decision) { const noteId = `note-${approvalId}-${decision}`; return ( -
+

{COPY[decision].body(workerName, toolLabel)}

- {decision === "approve" && payload !== undefined ? ( - // The dialog is narrow: wrap long strings (a message body) and scroll inside the view instead of stretching it. - - ) : null} + {payload !== undefined ? : null}
-