diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4a7ea30 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..699e321 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js 24 + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run verification gate + run: pnpm verify diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5731d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +node_modules/ +dist/ +coverage/ +*.log +.DS_Store +*.tmp +package-lock.json +*.tsbuildinfo +.idea/ +.superpowers/ +.vscode/ +Thumbs.db +Desktop.ini +*.swp +*.swo +*.bak +*.orig diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..8578380 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +coverage/ +pnpm-lock.yaml +.superpowers/ + diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..4cbc711 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2 +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd0120e --- /dev/null +++ b/README.md @@ -0,0 +1,118 @@ +# Relay + +Local task sidecar for human–AI workflows. + +> **Status:** Scaffold stage (Issue #1). Task tracking, companion skills, vendor integration configs, and packaging are explicitly deferred to subsequent issues (Issue #2+). + +## Prerequisites + +- Node.js `24.x` LTS (`.nvmrc`) + - Supported release line as of `2026-07-25`: Node `24.x` (`26.x` is Current, `25.x` is EOL) +- pnpm `10.2.0` (managed via Corepack) + +## Setup + +```bash +corepack enable +nvm use +pnpm install --frozen-lockfile +``` + +If you use `fnm`, `asdf`, or another version manager, switch to Node `24` before running install or verification. + +## Available Scripts + +- `pnpm verify` — **Non-mutating** aggregate quality gate. Executes `format:check -> lint -> typecheck -> test:coverage -> build -> validate:assets -> audit --audit-level high`. +- `pnpm format` — **Mutating**. Format codebase with Prettier. +- `pnpm format:check` — **Non-mutating**. Check formatting with Prettier. +- `pnpm lint` — **Non-mutating**. Run ESLint (`--max-warnings=0`). +- `pnpm typecheck` — **Non-mutating**. Perform strict TypeScript type checking (`tsc --build --noEmit`). +- `pnpm test` — **Non-mutating**. Run Vitest unit & integration tests once. +- `pnpm test:coverage` — **Non-mutating**. Run Vitest tests with V8 coverage threshold enforcement. +- `pnpm build:node` — **Mutating (dist/)**. Build Node backend entry points (`dist/mcp/main.js`, `dist/http/main.js`). +- `pnpm build:web` — **Mutating (dist/)**. Build Vite React web UI (`dist/web`). +- `pnpm build` — **Mutating (dist/)**. Run `build:node` and `build:web`. +- `pnpm dev:mcp` — **Non-mutating**. Run MCP stdio entry point from source via `tsx`. +- `pnpm dev:http` — **Non-mutating**. Run HTTP server from source (`http://127.0.0.1:43110`). +- `pnpm dev:web` — **Non-mutating**. Run Vite development server with proxy `/api` -> `http://127.0.0.1:43110`. +- `pnpm dev:ui` — **Non-mutating**. Run HTTP server and Vite development server concurrently. +- `pnpm validate:assets` — **Non-mutating**. Validate repository assets, package `bin`, and configuration. + +## Development Servers & Ports + +- Default HTTP loopback address: `127.0.0.1` +- Default HTTP port: `43110` (`GET /api/health`) +- Vite dev server port: `5173` (proxies `/api` to `http://127.0.0.1:43110`) + +## Configuration & Environment Variables + +- `RELAY_DB_PATH`: Custom file path to SQLite database. + - Windows default: `%APPDATA%\relay\relay.db` + - macOS default: `~/Library/Application Support/relay/relay.db` + - Linux default: `${XDG_DATA_HOME:-~/.local/share}/relay/relay.db` +- `RELAY_HTTP_PORT`: Custom port for loopback HTTP server (default: `43110`). + +## Database & Migrations + +Relay uses `better-sqlite3` with plain SQL migrations located under `src/database/migrations/`. + +On every database connection: + +- `PRAGMA foreign_keys = ON;` +- `PRAGMA journal_mode = WAL;` +- `PRAGMA busy_timeout = 5000;` + +Applied SQL migrations are tracked in `_relay_migrations` with SHA-256 checksums. **Applied migration SQL files are immutable**. + +## Invoking Built MCP Command Locally + +Build the scaffold Node entry points: + +```bash +pnpm build +``` + +Start the MCP stdio process: + +```bash +node dist/mcp/main.js +``` + +Or invoke via package binary entry point: + +```bash +./dist/mcp/main.js +``` + +The process exposes one scaffold health tool: `relay_health`. Diagnostics are written exclusively to `stderr`. + +## Architecture Boundaries + +```text +src/ + domain/ # Domain entities & rules (deferred to Issue #2+) + application/ # Application services (getHealth) + database/ # SQLite connection factory & migration runner + interfaces/ + mcp/ # MCP stdio server adapter (relay_health) + http/ # Loopback HTTP server adapter (GET /api/health) + shared/ # Custom errors & package metadata +web/ # Vite React 19 UI shell +``` + +Boundary rules: + +- `domain` and `application` layers have zero dependencies on interface protocols (`mcp`, `http`) or database implementations. +- `interfaces` call application services (`getHealth()`) and do not construct domain responses independently. +- `web/` calls loopback HTTP `/api/health` only and never imports Node modules. + +## Current Limitations + +- No task CRUD, task table, or product task behavior (deferred to Issue #2+). +- No companion skills, plugin manifests, or vendor MCP configs (deferred to Issue #2+). +- No remote network binding, authentication, multi-user accounts, background daemon, or desktop shell. + +## Troubleshooting + +- **`better-sqlite3` build issues:** Ensure Python and a C++ compiler build toolchain are installed if prebuilt binaries are unavailable. +- **Node version mismatch:** Relay supports Node.js `24.x` only. If your shell is on Node `25.x` or `26.x`, switch to Node `24` with `nvm use`, `fnm use 24`, or the equivalent command for your version manager before running `pnpm install` or `pnpm verify`. diff --git a/docs/decisions/0001-product-and-architecture.md b/docs/decisions/0001-product-and-architecture.md index 5e06388..4b1b294 100644 --- a/docs/decisions/0001-product-and-architecture.md +++ b/docs/decisions/0001-product-and-architecture.md @@ -273,4 +273,4 @@ TypeScript and Node.js were selected over Java because this product prioritizes Java could provide stronger runtime performance and structure, but those advantages are not material for a low-throughput local task queue. A conventional Java framework would add idle memory, startup, and packaging costs without corresponding product value. -The architecture remains intentionally evolutionary: SQLite and the application layer are authoritative, while MCP, CLI, and UI remain replaceable interfaces. \ No newline at end of file +The architecture remains intentionally evolutionary: SQLite and the application layer are authoritative, while MCP, CLI, and UI remain replaceable interfaces. diff --git a/docs/superpowers/plans/2026-07-25-scaffold-implementation-plan.md b/docs/superpowers/plans/2026-07-25-scaffold-implementation-plan.md new file mode 100644 index 0000000..51cf4c9 --- /dev/null +++ b/docs/superpowers/plans/2026-07-25-scaffold-implementation-plan.md @@ -0,0 +1,2019 @@ +# Scaffold Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Create a clean, production-shaped Relay scaffold in `D:\projects\relay` proving Node/TS ESM compilation, SQLite database with plain SQL migrations, an MCP stdio server with a `relay_health` tool, a loopback HTTP server with `GET /api/health`, a Vite/React shell UI displaying health status, repository asset validation, and a non-mutating `pnpm verify` CI gate. + +**Architecture:** A single private root Node.js/TypeScript ESM package with clean layer separation (`domain`, `application`, `database`, `interfaces/mcp`, `interfaces/http`, `shared`) and a frontend web shell (`web/`). Layer boundaries: `domain` and `application` have zero dependencies on interface protocols (`mcp`, `http`) or database implementations. Adapters call `getHealth()` for health state. + +**Tech Stack:** Node.js 24, pnpm (pinned via Corepack), TypeScript (strict ESM), `better-sqlite3`, `@modelcontextprotocol/sdk`, Zod, built-in `node:http`, React 19, Vite, Vitest, `@testing-library/react`, ESLint flat config, Prettier, `tsup`. + +## Global Constraints + +- Node version: `>=24 <25` strictly enforced via `.nvmrc` (`24`) and `package.json#engines`. +- Package manager: Pinned pnpm version with `package.json#packageManager` and committed `pnpm-lock.yaml`. +- ESM mandatory: `"type": "module"` in `package.json` across backend and frontend. +- Zero diagnostic output on stdout for MCP stdio process; all diagnostics use stderr via `logger.ts`. +- Loopback binding only (`127.0.0.1`) for HTTP server on default port `43110` (overridable via `RELAY_HTTP_PORT`). +- Database configuration precedence: explicit function argument > `RELAY_DB_PATH` > OS user-data directory default. +- SQLite PRAGMAs on every connection: `foreign_keys = ON`, `journal_mode = WAL`, `busy_timeout = 5000`. +- SQL migrations: transactional, checksummed with SHA-256, immutable once applied, executed in numeric prefix order. +- UI scope: minimal connectivity shell with loading, connected/version, failure, and retry states; no task CRUD or premature design system. +- Quality gate: `pnpm verify` runs `format:check -> lint -> typecheck -> test:coverage -> build -> validate:assets -> audit` non-mutatingly. +- Scope boundary: No ORM, web framework, daemon, authentication, multi-user tenancy, remote binding, agent skills, vendor configs, or task tables in Issue #1. + +--- + +### Task 1: Repository Scaffold & Package Metadata + +**Files:** + +- Create: `.nvmrc` +- Create: `.editorconfig` +- Create: `.gitignore` +- Create: `.prettierignore` +- Create: `.prettierrc.json` +- Create: `package.json` +- Create: `README.md` + +**Interfaces:** + +- Consumes: None +- Produces: Root package manifest, ignore files, editor rules, and formatted environment foundation. + +- [ ] **Step 1: Write `.nvmrc`** + +```text +24 +``` + +- [ ] **Step 2: Write `.editorconfig`** + +```ini +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false +``` + +- [ ] **Step 3: Write `.gitignore` and `.prettierignore`** + +`.gitignore`: + +```text +node_modules/ +dist/ +coverage/ +*.log +.DS_Store +*.tmp +``` + +`.prettierignore`: + +```text +node_modules/ +dist/ +coverage/ +pnpm-lock.yaml +``` + +- [ ] **Step 4: Write `.prettierrc.json`** + +```json +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2 +} +``` + +- [ ] **Step 5: Write initial `package.json`** + +```json +{ + "name": "relay", + "version": "0.1.0", + "description": "Local task sidecar for human–AI workflows", + "private": true, + "type": "module", + "engines": { + "node": ">=24 <25" + }, + "packageManager": "pnpm@10.2.0", + "bin": { + "relay-mcp": "./dist/mcp/main.js" + }, + "scripts": { + "format": "prettier --write .", + "format:check": "prettier --check ." + } +} +``` + +- [ ] **Step 6: Write initial `README.md` skeleton** + +````markdown +# Relay + +Local task sidecar for human–AI workflows. + +> **Status:** Scaffold stage (Issue #1). Task tracking, companion skills, and agent features are deferred to subsequent issues. + +## Prerequisites + +- Node.js 24.x LTS (`.nvmrc`) +- pnpm 10.2.0 (managed via Corepack) + +## Setup + +```bash +corepack enable +pnpm install +``` + +- [ ] **Step 7: Install initial dependencies and lock file** + +Run: `corepack enable && pnpm install` +Expected: `pnpm-lock.yaml` generated cleanly. + +- [ ] **Step 8: Verify formatting check** + +Run: `pnpm format:check` +Expected: `All matched files use Prettier code style!` + +- [ ] **Step 9: Commit** + +```bash +git add .nvmrc .editorconfig .gitignore .prettierignore .prettierrc.json package.json pnpm-lock.yaml README.md +git commit -m "chore: initialize repository scaffold and package metadata" +```` + +--- + +### Task 2: TypeScript Project Configurations & Layer Directory Structure + +**Files:** + +- Create: `tsconfig.base.json` +- Create: `tsconfig.json` +- Create: `tsconfig.node.json` +- Create: `tsconfig.web.json` +- Create: `tsconfig.test.json` +- Create: `src/domain/README.md` + +**Interfaces:** + +- Consumes: Package structure from Task 1 +- Produces: Strict TS configurations for Node, web, and tests; domain directory marker. + +- [ ] **Step 1: Write `tsconfig.base.json`** + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "useUnknownInCatchVariables": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + } +} +``` + +- [ ] **Step 2: Write `tsconfig.node.json`** + +```json +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["src/**/*.test.ts", "web/**/*"] +} +``` + +- [ ] **Step 3: Write `tsconfig.web.json`** + +```json +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "noEmit": true + }, + "include": ["web/src/**/*"] +} +``` + +- [ ] **Step 4: Write `tsconfig.test.json`** + +```json +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "moduleResolution": "Bundler", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["node", "vitest/globals"], + "noEmit": true + }, + "include": [ + "src/**/*", + "web/src/**/*", + "tests/**/*", + "scripts/**/*", + "*.config.ts", + "*.config.js" + ] +} +``` + +- [ ] **Step 5: Write `tsconfig.json` (Solution Configuration)** + +```json +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.web.json" }, + { "path": "./tsconfig.test.json" } + ] +} +``` + +- [ ] **Step 6: Write `src/domain/README.md`** + +```markdown +# Domain Layer + +Domain models, entities, and business rules for Relay. + +> **Note:** Domain task lifecycle rules and entities are intentionally deferred to subsequent issues (Issue #2+). +``` + +- [ ] **Step 7: Install TypeScript & Node Types** + +Run: `pnpm add -D typescript @types/node` +Expected: Packages added to `devDependencies`. + +- [ ] **Step 8: Commit** + +```bash +git add tsconfig*.json src/domain/README.md package.json pnpm-lock.yaml +git commit -m "chore: add strict TypeScript configuration and domain documentation boundary" +``` + +--- + +### Task 3: ESLint Flat Config & Code Quality Tooling + +**Files:** + +- Create: `eslint.config.js` +- Modify: `package.json` + +**Interfaces:** + +- Consumes: TypeScript setup from Task 2 +- Produces: `lint` and `typecheck` npm scripts with zero warning tolerance. + +- [ ] **Step 1: Install ESLint & TypeScript ESLint plugins** + +Run: `pnpm add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react-hooks eslint-plugin-react-refresh` +Expected: ESLint 9+ flat config tooling installed. + +- [ ] **Step 2: Write `eslint.config.js`** + +```js +import tsParser from '@typescript-eslint/parser'; +import tsPlugin from '@typescript-eslint/eslint-plugin'; +import reactHooks from 'eslint-plugin-react-hooks'; + +export default [ + { + ignores: ['dist/**', 'coverage/**', 'node_modules/**'], + }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js'], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.test.json'], + }, + }, + plugins: { + '@typescript-eslint': tsPlugin, + 'react-hooks': reactHooks, + }, + rules: { + ...tsPlugin.configs['recommended'].rules, + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': 'error', + '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }], + 'no-console': 'off', + }, + }, + { + files: ['src/interfaces/mcp/**/*.ts'], + rules: { + 'no-console': 'error', + }, + }, +]; +``` + +- [ ] **Step 3: Update `package.json` scripts** + +Add to `package.json#scripts`: + +```json +"lint": "eslint . --max-warnings=0", +"typecheck": "tsc --build --noEmit" +``` + +- [ ] **Step 4: Run lint and typecheck verification** + +Run: `pnpm lint && pnpm typecheck` +Expected: Both exit 0 with zero warnings. + +- [ ] **Step 5: Commit** + +```bash +git add eslint.config.js package.json pnpm-lock.yaml +git commit -m "tooling: configure ESLint flat config with type-checking and strict warning limits" +``` + +--- + +### Task 4: Shared Errors, Package Metadata & Health Application Contract + +**Files:** + +- Create: `src/shared/errors.ts` +- Create: `src/shared/package-metadata.ts` +- Create: `src/application/health/health.ts` +- Create: `src/application/health/get-health.ts` +- Create: `tests/unit/shared/package-metadata.test.ts` +- Create: `tests/unit/application/get-health.test.ts` + +**Interfaces:** + +- Consumes: Package name and version from `package.json` +- Produces: `getHealth(): HealthStatus` function returning `{ name: "relay", status: "ok", version: "0.1.0" }`. + +- [ ] **Step 1: Install Vitest** + +Run: `pnpm add -D vitest` +Expected: Vitest installed. + +- [ ] **Step 2: Create Vitest config `vitest.config.ts`** + +```ts +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['tests/**/*.test.ts', 'web/src/**/*.test.tsx'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + include: [ + 'src/application/**', + 'src/database/**', + 'src/interfaces/mcp/create-mcp-server.ts', + 'src/interfaces/http/create-http-server.ts', + 'web/src/api/**', + 'web/src/App.tsx', + ], + thresholds: { + statements: 80, + branches: 80, + functions: 80, + lines: 80, + }, + }, + }, +}); +``` + +- [ ] **Step 3: Write failing unit test for `package-metadata.ts`** + +`tests/unit/shared/package-metadata.test.ts`: + +```ts +import { describe, it, expect } from 'vitest'; +import { getPackageMetadata } from '../../../src/shared/package-metadata.js'; + +describe('package-metadata', () => { + it('returns package name and version', () => { + const meta = getPackageMetadata(); + expect(meta.name).toBe('relay'); + expect(meta.version).toMatch(/^\d+\.\d+\.\d+/); + }); +}); +``` + +- [ ] **Step 4: Run test to verify failure** + +Run: `pnpm vitest run tests/unit/shared/package-metadata.test.ts` +Expected: FAIL - module `package-metadata.js` not found. + +- [ ] **Step 5: Implement `src/shared/errors.ts` and `src/shared/package-metadata.ts`** + +`src/shared/errors.ts`: + +```ts +export class RelayError extends Error { + constructor( + message: string, + override readonly cause?: unknown, + ) { + super(message); + this.name = 'RelayError'; + } +} +``` + +`src/shared/package-metadata.ts`: + +```ts +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +export interface PackageMetadata { + readonly name: string; + readonly version: string; +} + +let cachedMetadata: PackageMetadata | null = null; + +export function getPackageMetadata(): PackageMetadata { + if (cachedMetadata) return cachedMetadata; + + const pkgPath = join(process.cwd(), 'package.json'); + const content = readFileSync(pkgPath, 'utf-8'); + const parsed = JSON.parse(content) as { name: string; version: string }; + + cachedMetadata = { + name: parsed.name, + version: parsed.version, + }; + return cachedMetadata; +} +``` + +- [ ] **Step 6: Run metadata test to verify pass** + +Run: `pnpm vitest run tests/unit/shared/package-metadata.test.ts` +Expected: PASS. + +- [ ] **Step 7: Write failing unit test for `getHealth()`** + +`tests/unit/application/get-health.test.ts`: + +```ts +import { describe, it, expect } from 'vitest'; +import { getHealth } from '../../../src/application/health/get-health.js'; + +describe('getHealth', () => { + it('returns exact deterministic health status contract', () => { + const health = getHealth(); + expect(health).toEqual({ + name: 'relay', + status: 'ok', + version: '0.1.0', + }); + }); +}); +``` + +- [ ] **Step 8: Implement `src/application/health/health.ts` and `src/application/health/get-health.ts`** + +`src/application/health/health.ts`: + +```ts +export interface HealthStatus { + readonly name: 'relay'; + readonly status: 'ok'; + readonly version: string; +} +``` + +`src/application/health/get-health.ts`: + +```ts +import type { HealthStatus } from './health.js'; +import { getPackageMetadata } from '../../shared/package-metadata.js'; + +export function getHealth(): HealthStatus { + const meta = getPackageMetadata(); + return { + name: 'relay', + status: 'ok', + version: meta.version, + }; +} +``` + +- [ ] **Step 9: Run tests to verify pass** + +Run: `pnpm vitest run tests/unit/application/get-health.test.ts` +Expected: PASS. + +- [ ] **Step 10: Update `package.json` test scripts** + +```json +"test": "vitest run", +"test:coverage": "vitest run --coverage" +``` + +- [ ] **Step 11: Commit** + +```bash +git add src/shared/ src/application/ vitest.config.ts tests/package.json pnpm-lock.yaml +git commit -m "feat(application): implement health status application contract and package metadata helper" +``` + +--- + +### Task 5: SQLite Database Configuration, Connection Factory & Plain SQL Migration Runner + +**Files:** + +- Create: `src/database/database-config.ts` +- Create: `src/database/connection.ts` +- Create: `src/database/migration.ts` +- Create: `src/database/migrate.ts` +- Create: `src/database/migrations/0001_scaffold.sql` +- Create: `tests/support/temporary-database.ts` +- Create: `tests/unit/database/database-config.test.ts` +- Create: `tests/unit/database/migration.test.ts` +- Create: `tests/integration/database-migrations.test.ts` + +**Interfaces:** + +- Consumes: Environment variable `RELAY_DB_PATH` or explicit path parameter +- Produces: Configured `better-sqlite3` Database connection with WAL/Foreign Keys/Busy Timeout, and transactional SHA-256 migration runner. + +- [ ] **Step 1: Install `better-sqlite3` and type definitions** + +Run: `pnpm add better-sqlite3 && pnpm add -D @types/better-sqlite3` +Expected: SQLite driver installed. + +- [ ] **Step 2: Write failing unit test for `database-config.ts`** + +`tests/unit/database/database-config.test.ts`: + +```ts +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { resolveDatabasePath } from '../../../src/database/database-config.js'; + +describe('resolveDatabasePath', () => { + const origEnv = process.env.RELAY_DB_PATH; + + afterEach(() => { + if (origEnv !== undefined) { + process.env.RELAY_DB_PATH = origEnv; + } else { + delete process.env.RELAY_DB_PATH; + } + }); + + it('prefers explicit argument over environment variable', () => { + process.env.RELAY_DB_PATH = '/env/path.db'; + const path = resolveDatabasePath('/explicit/path.db'); + expect(path).toBe('/explicit/path.db'); + }); + + it('uses RELAY_DB_PATH env var when no explicit path passed', () => { + process.env.RELAY_DB_PATH = '/env/path.db'; + const path = resolveDatabasePath(); + expect(path).toBe('/env/path.db'); + }); + + it('rejects empty or whitespace-only explicit path', () => { + expect(() => resolveDatabasePath('')).toThrow(); + expect(() => resolveDatabasePath(' ')).toThrow(); + }); +}); +``` + +- [ ] **Step 3: Implement `src/database/database-config.ts`** + +```ts +import { join } from 'node:path'; +import { homedir } from 'node:os'; +import { RelayError } from '../shared/errors.js'; + +export function getDefaultDatabasePath(): string { + const home = homedir(); + if (process.platform === 'win32') { + const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming'); + return join(appData, 'relay', 'relay.db'); + } + if (process.platform === 'darwin') { + return join(home, 'Library', 'Application Support', 'relay', 'relay.db'); + } + const xdgData = process.env.XDG_DATA_HOME || join(home, '.local', 'share'); + return join(xdgData, 'relay', 'relay.db'); +} + +export function resolveDatabasePath(explicitPath?: string): string { + if (explicitPath !== undefined) { + if (!explicitPath.trim()) { + throw new RelayError('Database path cannot be empty or whitespace only.'); + } + return explicitPath.trim(); + } + + const envPath = process.env.RELAY_DB_PATH; + if (envPath && envPath.trim()) { + return envPath.trim(); + } + + return getDefaultDatabasePath(); +} +``` + +- [ ] **Step 4: Implement `src/database/connection.ts`** + +```ts +import Database from 'better-sqlite3'; +import { mkdirSync } from 'node:fs'; +import { dirname } from 'node:path'; +import { resolveDatabasePath } from './database-config.js'; + +export interface DatabaseConnectionOptions { + readonly path?: string; + readonly readonly?: boolean; +} + +export function createDatabaseConnection( + options: DatabaseConnectionOptions = {}, +): Database.Database { + const dbPath = resolveDatabasePath(options.path); + + if (dbPath !== ':memory:') { + mkdirSync(dirname(dbPath), { recursive: true }); + } + + const db = new Database(dbPath, { readonly: options.readonly }); + + db.pragma('foreign_keys = ON'); + db.pragma('journal_mode = WAL'); + db.pragma('busy_timeout = 5000'); + + return db; +} +``` + +- [ ] **Step 5: Write scaffold SQL migration `src/database/migrations/0001_scaffold.sql`** + +```sql +CREATE TABLE IF NOT EXISTS relay_metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +INSERT OR IGNORE INTO relay_metadata (key, value) VALUES ('schema_version', '1'); +``` + +- [ ] **Step 6: Implement migration loader and runner `src/database/migration.ts` & `src/database/migrate.ts`** + +`src/database/migration.ts`: + +```ts +import { createHash } from 'node:hash'; +import { readdirSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { RelayError } from '../shared/errors.js'; + +export interface MigrationFile { + readonly version: number; + readonly name: string; + readonly filename: string; + readonly sql: string; + readonly checksum: string; +} + +export function computeChecksum(content: string): string { + return createHash('sha256').update(content, 'utf-8').digest('hex'); +} + +export function loadMigrationFiles(migrationsDir: string): readonly MigrationFile[] { + const entries = readdirSync(migrationsDir, { withFileTypes: true }); + const files: MigrationFile[] = []; + const seenVersions = new Set(); + + for (const entry of entries) { + if (!entry.isFile() || !entry.name.endsWith('.sql')) continue; + + const match = /^(\d{4})_(.+)\.sql$/.exec(entry.name); + if (!match) { + throw new RelayError( + `Malformed migration filename: ${entry.name}. Expected format: NNNN_description.sql`, + ); + } + + const versionStr = match[1]; + const name = match[2]; + if (!versionStr || !name) continue; + + const version = parseInt(versionStr, 10); + if (seenVersions.has(version)) { + throw new RelayError(`Duplicate migration version prefix: ${versionStr}`); + } + seenVersions.add(version); + + const fullPath = join(migrationsDir, entry.name); + const sql = readFileSync(fullPath, 'utf-8'); + const checksum = computeChecksum(sql); + + files.push({ version, name, filename: entry.name, sql, checksum }); + } + + return files.sort((a, b) => a.version - b.version); +} +``` + +`src/database/migrate.ts`: + +```ts +import type Database from 'better-sqlite3'; +import { join } from 'node:path'; +import { loadMigrationFiles } from './migration.js'; +import { RelayError } from '../shared/errors.js'; + +export interface MigrationOptions { + readonly migrationsDir?: string; +} + +export function runMigrations(db: Database.Database, options: MigrationOptions = {}): void { + const migrationsDir = + options.migrationsDir || join(process.cwd(), 'src', 'database', 'migrations'); + + db.exec(` + CREATE TABLE IF NOT EXISTS _relay_migrations ( + version INTEGER PRIMARY KEY, + name TEXT NOT NULL, + checksum TEXT NOT NULL, + applied_at TEXT NOT NULL + ); + `); + + const appliedRows = db + .prepare('SELECT version, name, checksum FROM _relay_migrations ORDER BY version ASC') + .all() as { + version: number; + name: string; + checksum: string; + }[]; + + const appliedMap = new Map(appliedRows.map((r) => [r.version, r])); + const migrationFiles = loadMigrationFiles(migrationsDir); + + for (const file of migrationFiles) { + const applied = appliedMap.get(file.version); + if (applied) { + if (applied.checksum !== file.checksum || applied.name !== file.name) { + throw new RelayError( + `Migration mismatch for version ${file.version} (${file.filename}). ` + + `Applied checksum/name does not match repository SQL file. Applied SQL files are immutable.`, + ); + } + continue; + } + + const applyTransaction = db.transaction(() => { + db.exec(file.sql); + db.prepare( + `INSERT INTO _relay_migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)`, + ).run(file.version, file.name, file.checksum); + }); + + applyTransaction(); + } +} +``` + +- [ ] **Step 7: Implement temporary database test helper `tests/support/temporary-database.ts`** + +```ts +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import type Database from 'better-sqlite3'; +import { createDatabaseConnection } from '../../src/database/connection.js'; + +export interface TemporaryDatabaseContext { + readonly dir: string; + readonly dbPath: string; + readonly db: Database.Database; + readonly cleanup: () => void; +} + +export function createTemporaryDatabase(): TemporaryDatabaseContext { + const dir = mkdtempSync(join(tmpdir(), 'relay-test-')); + const dbPath = join(dir, 'test.db'); + const db = createDatabaseConnection({ path: dbPath }); + + const cleanup = () => { + try { + db.close(); + } catch { + // ignore + } + rmSync(dir, { recursive: true, force: true }); + }; + + return { dir, dbPath, db, cleanup }; +} +``` + +- [ ] **Step 8: Write database integration tests `tests/integration/database-migrations.test.ts`** + +```ts +import { describe, it, expect, afterEach } from 'vitest'; +import { createTemporaryDatabase } from '../support/temporary-database.js'; +import { runMigrations } from '../../src/database/migrate.js'; + +describe('database-migrations integration', () => { + let tempDb: ReturnType | null = null; + + afterEach(() => { + tempDb?.cleanup(); + tempDb = null; + }); + + it('runs migrations on a fresh temporary SQLite database and verifies PRAGMAs', () => { + tempDb = createTemporaryDatabase(); + const { db } = tempDb; + + runMigrations(db); + + const fk = db.pragma('foreign_keys', { simple: true }); + const jm = db.pragma('journal_mode', { simple: true }); + const bt = db.pragma('busy_timeout', { simple: true }); + + expect(fk).toBe(1); + expect(jm).toBe('wal'); + expect(bt).toBe(5000); + + const migrations = db.prepare('SELECT version, name FROM _relay_migrations').all() as { + version: number; + name: string; + }[]; + expect(migrations).toHaveLength(1); + expect(migrations[0]?.version).toBe(1); + expect(migrations[0]?.name).toBe('scaffold'); + + // Idempotence test + expect(() => runMigrations(db)).not.toThrow(); + }); +}); +``` + +- [ ] **Step 9: Run database unit and integration tests** + +Run: `pnpm vitest run tests/unit/database tests/integration/database-migrations.test.ts` +Expected: ALL PASS. + +- [ ] **Step 10: Commit** + +```bash +git add src/database/ tests/database tests/support/package.json pnpm-lock.yaml +git commit -m "feat(database): implement SQLite connection factory, PRAGMAs, and transactional SHA-256 SQL migration runner" +``` + +--- + +### Task 6: MCP Server Factory & In-Process Integration Test + +**Files:** + +- Create: `src/interfaces/mcp/logger.ts` +- Create: `src/interfaces/mcp/create-mcp-server.ts` +- Create: `tests/unit/interfaces/mcp/create-mcp-server.test.ts` + +**Interfaces:** + +- Consumes: `getHealth()` from application layer +- Produces: `createMcpServer()` factory exposing `relay_health` tool, and `logger.ts` writing only to stderr. + +- [ ] **Step 1: Install `@modelcontextprotocol/sdk` and `zod`** + +Run: `pnpm add @modelcontextprotocol/sdk zod` +Expected: MCP SDK and Zod installed. + +- [ ] **Step 2: Write stderr logger `src/interfaces/mcp/logger.ts`** + +```ts +export const mcpLogger = { + info(message: string): void { + process.stderr.write(`[INFO] ${message}\n`); + }, + error(message: string, error?: unknown): void { + const detail = error instanceof Error ? `: ${error.message}` : ''; + process.stderr.write(`[ERROR] ${message}${detail}\n`); + }, +}; +``` + +- [ ] **Step 3: Write failing unit test for `createMcpServer`** + +`tests/unit/interfaces/mcp/create-mcp-server.test.ts`: + +```ts +import { describe, it, expect } from 'vitest'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { createMcpServer } from '../../../../src/interfaces/mcp/create-mcp-server.js'; + +describe('createMcpServer', () => { + it('exposes relay_health tool via in-memory transport', async () => { + const server = createMcpServer(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + const client = new Client({ name: 'test-client', version: '1.0.0' }); + + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]); + + const tools = await client.listTools(); + expect(tools.tools.some((t) => t.name === 'relay_health')).toBe(true); + + const result = await client.callTool({ name: 'relay_health', arguments: {} }); + expect(result.content[0]?.type).toBe('text'); + if (result.content[0]?.type === 'text') { + const parsed = JSON.parse(result.content[0].text); + expect(parsed).toEqual({ name: 'relay', status: 'ok', version: '0.1.0' }); + } + }); +}); +``` + +- [ ] **Step 4: Implement `src/interfaces/mcp/create-mcp-server.ts`** + +```ts +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { getHealth } from '../../application/health/get-health.js'; +import { getPackageMetadata } from '../../shared/package-metadata.js'; +import { z } from 'zod'; + +export function createMcpServer(): McpServer { + const meta = getPackageMetadata(); + const server = new McpServer({ + name: meta.name, + version: meta.version, + }); + + server.tool('relay_health', 'Return health status of the local Relay service', {}, async () => { + const health = getHealth(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(health), + }, + ], + }; + }); + + return server; +} +``` + +- [ ] **Step 5: Run unit test to verify pass** + +Run: `pnpm vitest run tests/unit/interfaces/mcp/create-mcp-server.test.ts` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/interfaces/mcp/ tests/unit/interfaces/mcp/ package.json pnpm-lock.yaml +git commit -m "feat(mcp): implement MCP server factory exposing relay_health tool over SDK in-memory transport" +``` + +--- + +### Task 7: MCP Stdio Entry Point, Build Setup & Built-Process Integration Test + +**Files:** + +- Create: `src/interfaces/mcp/main.ts` +- Create: `tsup.config.ts` +- Modify: `package.json` +- Create: `tests/integration/mcp-stdio.test.ts` + +**Interfaces:** + +- Consumes: `createMcpServer()` factory +- Produces: `dist/mcp/main.js` built executable, and `pnpm dev:mcp` command. + +- [ ] **Step 1: Install `tsup`** + +Run: `pnpm add -D tsup` +Expected: `tsup` build tool installed. + +- [ ] **Step 2: Create `tsup.config.ts`** + +```ts +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/interfaces/mcp/main.ts', 'src/interfaces/http/main.ts'], + format: ['esm'], + target: 'node24', + outDir: 'dist', + clean: true, + sourcemap: true, + bundle: true, + shims: true, +}); +``` + +- [ ] **Step 3: Implement `src/interfaces/mcp/main.ts`** + +```ts +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { createMcpServer } from './create-mcp-server.js'; +import { mcpLogger } from './logger.js'; + +async function main(): Promise { + try { + const server = createMcpServer(); + const transport = new StdioServerTransport(); + + process.on('SIGINT', () => { + mcpLogger.info('Received SIGINT, shutting down MCP server...'); + process.exit(0); + }); + + process.on('SIGTERM', () => { + mcpLogger.info('Received SIGTERM, shutting down MCP server...'); + process.exit(0); + }); + + await server.connect(transport); + } catch (error) { + mcpLogger.error('Fatal error starting MCP stdio server', error); + process.exit(1); + } +} + +void main(); +``` + +- [ ] **Step 4: Update `package.json` scripts for `build:node` and `dev:mcp`** + +Add scripts: + +```json +"build:node": "tsup", +"dev:mcp": "node --import tsx/esm src/interfaces/mcp/main.ts" +``` + +Install `tsx` for ts node running: `pnpm add -D tsx` + +- [ ] **Step 5: Run `pnpm build:node` and verify build output** + +Run: `pnpm build:node` +Expected: `dist/mcp/main.js` generated. + +- [ ] **Step 6: Write integration test for built MCP stdio process `tests/integration/mcp-stdio.test.ts`** + +```ts +import { describe, it, expect, beforeAll } from 'vitest'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { join } from 'node:path'; +import { execSync } from 'node:child_process'; + +describe('mcp-stdio integration', () => { + beforeAll(() => { + execSync('pnpm build:node', { stdio: 'inherit' }); + }); + + it('spawns built MCP stdio process and calls relay_health tool cleanly', async () => { + const builtJsPath = join(process.cwd(), 'dist', 'mcp', 'main.js'); + + const transport = new StdioClientTransport({ + command: 'node', + args: [builtJsPath], + }); + + const client = new Client({ name: 'integration-tester', version: '1.0.0' }); + + await client.connect(transport); + + const tools = await client.listTools(); + expect(tools.tools.some((t) => t.name === 'relay_health')).toBe(true); + + const res = await client.callTool({ name: 'relay_health', arguments: {} }); + expect(res.content[0]?.type).toBe('text'); + if (res.content[0]?.type === 'text') { + const payload = JSON.parse(res.content[0].text); + expect(payload).toEqual({ name: 'relay', status: 'ok', version: '0.1.0' }); + } + + await transport.close(); + }); +}); +``` + +- [ ] **Step 7: Run integration test** + +Run: `pnpm vitest run tests/integration/mcp-stdio.test.ts` +Expected: PASS. + +- [ ] **Step 8: Commit** + +```bash +git add src/interfaces/mcp/main.ts tsup.config.ts tests/integration/mcp-stdio.test.ts package.json pnpm-lock.yaml +git commit -m "feat(mcp): add MCP stdio executable entry point, tsup build configuration, and stdio integration test" +``` + +--- + +### Task 8: Loopback HTTP Server Factory, Entry Point & Integration Tests + +**Files:** + +- Create: `src/interfaces/http/create-http-server.ts` +- Create: `src/interfaces/http/main.ts` +- Create: `tests/integration/http-health.test.ts` +- Modify: `package.json` + +**Interfaces:** + +- Consumes: `getHealth()` application contract +- Produces: `node:http` server binding to `127.0.0.1` exposing `GET /api/health`, and `pnpm dev:http` script. + +- [ ] **Step 1: Write failing integration test `tests/integration/http-health.test.ts`** + +```ts +import { describe, it, expect, afterEach } from 'vitest'; +import { + createHttpServer, + type HttpServerInstance, +} from '../../src/interfaces/http/create-http-server.js'; + +describe('http-health integration', () => { + let serverInstance: HttpServerInstance | null = null; + + afterEach(async () => { + if (serverInstance) { + await serverInstance.stop(); + serverInstance = null; + } + }); + + it('starts on 127.0.0.1 and returns 200 for GET /api/health', async () => { + serverInstance = await createHttpServer({ host: '127.0.0.1', port: 0 }); + const { url } = serverInstance; + + const res = await fetch(`${url}/api/health`); + expect(res.status).toBe(200); + expect(res.headers.get('content-type')).toContain('application/json'); + + const body = await res.json(); + expect(body).toEqual({ name: 'relay', status: 'ok', version: '0.1.0' }); + }); + + it('returns 405 Method Not Allowed for POST /api/health', async () => { + serverInstance = await createHttpServer({ host: '127.0.0.1', port: 0 }); + const { url } = serverInstance; + + const res = await fetch(`${url}/api/health`, { method: 'POST' }); + expect(res.status).toBe(405); + expect(res.headers.get('allow')).toBe('GET'); + }); + + it('returns 404 Not Found for unknown routes', async () => { + serverInstance = await createHttpServer({ host: '127.0.0.1', port: 0 }); + const { url } = serverInstance; + + const res = await fetch(`${url}/unknown-route`); + expect(res.status).toBe(404); + const body = await res.json(); + expect(body).toEqual({ error: 'not_found' }); + }); +}); +``` + +- [ ] **Step 2: Implement `src/interfaces/http/create-http-server.ts`** + +```ts +import { createServer, type IncomingMessage, type ServerResponse, type Server } from 'node:http'; +import { getHealth } from '../../application/health/get-health.js'; +import { RelayError } from '../../shared/errors.js'; + +export interface HttpServerOptions { + readonly host?: string; + readonly port?: number; +} + +export interface HttpServerInstance { + readonly server: Server; + readonly host: string; + readonly port: number; + readonly url: string; + readonly stop: () => Promise; +} + +export function resolveHttpPort(explicitPort?: number): number { + if (explicitPort !== undefined) { + if (explicitPort < 0 || explicitPort > 65535) { + throw new RelayError(`Invalid HTTP port: ${explicitPort}. Must be between 0 and 65535.`); + } + return explicitPort; + } + + const envPort = process.env.RELAY_HTTP_PORT; + if (envPort) { + const parsed = parseInt(envPort, 10); + if (isNaN(parsed) || parsed < 1 || parsed > 65535) { + throw new RelayError(`Invalid RELAY_HTTP_PORT environment variable: ${envPort}.`); + } + return parsed; + } + + return 43110; +} + +export function createHttpServer(options: HttpServerOptions = {}): Promise { + const host = options.host || '127.0.0.1'; + const port = resolveHttpPort(options.port); + + if (host !== '127.0.0.1' && host !== 'localhost') { + throw new RelayError( + `Loopback security restriction: HTTP server host must be 127.0.0.1 or localhost (got ${host}).`, + ); + } + + const requestHandler = (req: IncomingMessage, res: ServerResponse) => { + const url = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`); + + if (url.pathname === '/api/health') { + if (req.method !== 'GET') { + res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8', Allow: 'GET' }); + res.end(JSON.stringify({ error: 'method_not_allowed' })); + return; + } + + const health = getHealth(); + res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); + res.end(JSON.stringify(health)); + return; + } + + res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' }); + res.end(JSON.stringify({ error: 'not_found' })); + }; + + const server = createServer(requestHandler); + + return new Promise((resolve, reject) => { + server.on('error', (err) => reject(new RelayError('HTTP server error', err))); + + server.listen(port, host, () => { + const addr = server.address(); + const actualPort = typeof addr === 'object' && addr ? addr.port : port; + const serverUrl = `http://${host}:${actualPort}`; + + const stop = (): Promise => { + return new Promise((resStop, rejStop) => { + server.close((err) => { + if (err) rejStop(err); + else resStop(); + }); + }); + }; + + resolve({ + server, + host, + port: actualPort, + url: serverUrl, + stop, + }); + }); + }); +} +``` + +- [ ] **Step 3: Implement HTTP entry point `src/interfaces/http/main.ts`** + +```ts +import { createHttpServer } from './create-http-server.js'; + +async function main(): Promise { + try { + const instance = await createHttpServer(); + process.stderr.write(`[INFO] HTTP server running at ${instance.url}\n`); + + const shutdown = () => { + process.stderr.write('[INFO] Stopping HTTP server...\n'); + void instance.stop().then(() => process.exit(0)); + }; + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + process.stderr.write(`[ERROR] Fatal HTTP server error: ${msg}\n`); + process.exit(1); + } +} + +void main(); +``` + +- [ ] **Step 4: Update `package.json` for `dev:http`** + +Add script: + +```json +"dev:http": "node --import tsx/esm src/interfaces/http/main.ts" +``` + +- [ ] **Step 5: Run HTTP integration tests** + +Run: `pnpm vitest run tests/integration/http-health.test.ts` +Expected: ALL PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/interfaces/http/ tests/integration/http-health.test.ts package.json pnpm-lock.yaml +git commit -m "feat(http): implement loopback node:http server factory with GET /api/health route and integration tests" +``` + +--- + +### Task 9: Vite React Shell & Health Client + +**Files:** + +- Create: `web/index.html` +- Create: `web/src/vite-env.d.ts` +- Create: `web/src/api/health-client.ts` +- Create: `web/src/App.tsx` +- Create: `web/src/main.tsx` +- Create: `vite.config.ts` +- Create: `tests/unit/web/health-client.test.ts` +- Create: `web/src/App.test.tsx` +- Modify: `package.json` + +**Interfaces:** + +- Consumes: HTTP endpoint `/api/health` +- Produces: React 19 connectivity shell displaying loading, connected/version, and failure/retry states; `pnpm dev:web` and `pnpm dev:ui` scripts. + +- [ ] **Step 1: Install React 19, Vite, and Testing Library** + +Run: `pnpm add react react-dom && pnpm add -D vite @vitejs/plugin-react @testing-library/react @testing-library/jest-dom jsdom` + +- [ ] **Step 2: Create `vite.config.ts`** + +```ts +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + root: 'web', + build: { + outDir: '../dist/web', + emptyOutDir: true, + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://127.0.0.1:43110', + changeOrigin: true, + }, + }, + }, +}); +``` + +- [ ] **Step 3: Create `web/index.html` and `web/src/vite-env.d.ts`** + +`web/index.html`: + +```html + + + + + + Relay + + +
+ + + +``` + +`web/src/vite-env.d.ts`: + +```ts +/// +``` + +- [ ] **Step 4: Implement browser health client `web/src/api/health-client.ts`** + +```ts +import { z } from 'zod'; + +export const HealthStatusSchema = z.object({ + name: z.literal('relay'), + status: z.literal('ok'), + version: z.string(), +}); + +export type HealthStatusResponse = z.infer; + +export async function fetchHealth(signal?: AbortSignal): Promise { + const res = await fetch('/api/health', { signal }); + if (!res.ok) { + throw new Error(`Health check failed with status ${res.status}`); + } + const data = await res.json(); + const parsed = HealthStatusSchema.safeParse(data); + if (!parsed.success) { + throw new Error('Invalid health check response schema'); + } + return parsed.data; +} +``` + +- [ ] **Step 5: Write unit test `tests/unit/web/health-client.test.ts`** + +```ts +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { fetchHealth } from '../../../web/src/api/health-client.js'; + +describe('health-client', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('parses valid /api/health response', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ name: 'relay', status: 'ok', version: '0.1.0' }), + }), + ); + + const health = await fetchHealth(); + expect(health).toEqual({ name: 'relay', status: 'ok', version: '0.1.0' }); + }); + + it('throws on non-2xx response', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: false, + status: 500, + }), + ); + + await expect(fetchHealth()).rejects.toThrow('Health check failed with status 500'); + }); +}); +``` + +- [ ] **Step 6: Implement `web/src/App.tsx`** + +```tsx +import { useEffect, useState, useCallback } from 'react'; +import { fetchHealth, type HealthStatusResponse } from './api/health-client.js'; + +export function App() { + const [health, setHealth] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const loadHealth = useCallback(() => { + setLoading(true); + setError(null); + const controller = new AbortController(); + + fetchHealth(controller.signal) + .then((data) => { + setHealth(data); + setLoading(false); + }) + .catch((err) => { + if (err.name === 'AbortError') return; + setError(err instanceof Error ? err.message : 'Unknown error'); + setLoading(false); + }); + + return () => controller.abort(); + }, []); + + useEffect(() => { + return loadHealth(); + }, [loadHealth]); + + return ( +
+

Relay

+

Local task sidecar for human–AI workflows.

+ +
+ {loading &&

Checking local service…

} + {!loading && error && ( +
+

Relay service unavailable

+ +
+ )} + {!loading && health &&

Connected (v{health.version})

} +
+
+ ); +} +``` + +- [ ] **Step 7: Implement `web/src/main.tsx`** + +```tsx +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App.js'; + +const rootEl = document.getElementById('root'); +if (rootEl) { + createRoot(rootEl).render( + + + , + ); +} +``` + +- [ ] **Step 8: Write UI smoke test `web/src/App.test.tsx`** + +```tsx +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { App } from './App.js'; +import * as healthClient from './api/health-client.js'; + +describe('App component', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('renders loading, then success state', async () => { + vi.spyOn(healthClient, 'fetchHealth').mockResolvedValue({ + name: 'relay', + status: 'ok', + version: '0.1.0', + }); + + render(); + + expect(screen.getByTestId('status-loading')).toBeDefined(); + + await waitFor(() => { + expect(screen.getByTestId('status-success')).toBeDefined(); + }); + expect(screen.getByText('Connected (v0.1.0)')).toBeDefined(); + }); + + it('renders error state and handles retry button click', async () => { + const fetchSpy = vi + .spyOn(healthClient, 'fetchHealth') + .mockRejectedValueOnce(new Error('Connection refused')) + .mockResolvedValueOnce({ name: 'relay', status: 'ok', version: '0.1.0' }); + + render(); + + await waitFor(() => { + expect(screen.getByTestId('status-error')).toBeDefined(); + }); + + const retryBtn = screen.getByText('Retry'); + fireEvent.click(retryBtn); + + await waitFor(() => { + expect(screen.getByTestId('status-success')).toBeDefined(); + }); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); +}); +``` + +- [ ] **Step 9: Install concurrently for `dev:ui` and update `package.json` scripts** + +Run: `pnpm add -D concurrently` + +Add scripts: + +```json +"dev:web": "vite", +"dev:ui": "concurrently -k -p name -c \"blue,green\" \"pnpm dev:http\" \"pnpm dev:web\"" +``` + +- [ ] **Step 10: Run frontend tests** + +Run: `pnpm vitest run tests/unit/web/health-client.test.ts web/src/App.test.tsx` +Expected: ALL PASS. + +- [ ] **Step 11: Commit** + +```bash +git add web/ vite.config.ts tests/unit/web/ package.json pnpm-lock.yaml +git commit -m "feat(web): add minimal Vite React 19 connectivity shell displaying HTTP health status" +``` + +--- + +### Task 10: Node & Web Production Build Integration + +**Files:** + +- Modify: `package.json` + +**Interfaces:** + +- Consumes: Source code under `src/` and `web/` +- Produces: `dist/mcp/main.js`, `dist/http/main.js`, and `dist/web/index.html` static bundle. + +- [ ] **Step 1: Configure build scripts in `package.json`** + +```json +"build:node": "tsup", +"build:web": "vite build", +"build": "pnpm build:node && pnpm build:web" +``` + +- [ ] **Step 2: Execute `pnpm build`** + +Run: `pnpm build` +Expected: Outputs generated in `dist/mcp/main.js`, `dist/http/main.js`, and `dist/web/`. + +- [ ] **Step 3: Verify built outputs exist** + +Run: `node -e "console.log(fs.existsSync('dist/mcp/main.js') && fs.existsSync('dist/http/main.js') && fs.existsSync('dist/web/index.html'))"` +Expected: `true`. + +- [ ] **Step 4: Commit** + +```bash +git add package.json +git commit -m "build: configure combined Node and Vite web production build scripts" +``` + +--- + +### Task 11: Repository Asset Validator + +**Files:** + +- Create: `scripts/validate-repository-assets.ts` +- Modify: `package.json` + +**Interfaces:** + +- Consumes: Repository files, `package.json#bin`, README local markdown links +- Produces: `validate:assets` script asserting repository structure and document integrity. + +- [ ] **Step 1: Write `scripts/validate-repository-assets.ts`** + +```ts +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +function fail(msg: string): never { + process.stderr.write(`[ASSET VALIDATION FAILURE] ${msg}\n`); + process.exit(1); +} + +function validateAssets(): void { + const cwd = process.cwd(); + + // 1. Required files/directories + const requiredPaths = [ + '.nvmrc', + '.editorconfig', + '.gitignore', + '.prettierrc.json', + 'eslint.config.js', + 'package.json', + 'README.md', + 'tsconfig.base.json', + 'src/application/health/get-health.ts', + 'src/database/connection.ts', + 'src/interfaces/mcp/create-mcp-server.ts', + 'src/interfaces/http/create-http-server.ts', + 'web/src/App.tsx', + ]; + + for (const p of requiredPaths) { + if (!existsSync(join(cwd, p))) { + fail(`Required path missing: ${p}`); + } + } + + // 2. package.json bin validation + const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8')) as { + bin?: Record; + }; + const binRelayMcp = pkg.bin?.['relay-mcp']; + if (binRelayMcp !== './dist/mcp/main.js') { + fail(`package.json#bin.relay-mcp must point to ./dist/mcp/main.js (got ${binRelayMcp})`); + } + + // 3. No SKILL.md or agent configs in #1 + const forbidden = ['SKILL.md', 'agent/skills', 'agent/mcp']; + for (const f of forbidden) { + if (existsSync(join(cwd, f))) { + fail(`Forbidden asset for Issue #1 present: ${f}`); + } + } + + process.stdout.write('Repository asset validation passed successfully.\n'); +} + +validateAssets(); +``` + +- [ ] **Step 2: Add `validate:assets` script to `package.json`** + +```json +"validate:assets": "node --import tsx/esm scripts/validate-repository-assets.ts" +``` + +- [ ] **Step 3: Run `validate:assets`** + +Run: `pnpm validate:assets` +Expected: `Repository asset validation passed successfully.` + +- [ ] **Step 4: Commit** + +```bash +git add scripts/validate-repository-assets.ts package.json +git commit -m "chore(scripts): implement repository asset validator script" +``` + +--- + +### Task 12: Non-Mutating Aggregate Verification Gate, Audit & GitHub Actions CI Workflow + +**Files:** + +- Create: `.github/workflows/ci.yml` +- Modify: `package.json` + +**Interfaces:** + +- Consumes: All quality scripts (`format:check`, `lint`, `typecheck`, `test:coverage`, `build`, `validate:assets`, `audit`) +- Produces: `pnpm verify` command and automated `.github/workflows/ci.yml` PR workflow. + +- [ ] **Step 1: Add `audit` and `verify` scripts to `package.json`** + +```json +"audit": "pnpm audit --audit-level high", +"verify": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test:coverage && pnpm build && pnpm validate:assets && pnpm audit" +``` + +- [ ] **Step 2: Test `pnpm verify` locally** + +Run: `pnpm verify` +Expected: Passes format check, lint, typecheck, coverage, build, asset validation, and audit cleanly. + +- [ ] **Step 3: Verify git status is completely clean after `pnpm verify`** + +Run: `git status --short` +Expected: Empty output (non-mutating). + +- [ ] **Step 4: Write GitHub Actions workflow `.github/workflows/ci.yml`** + +```yaml +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js 24 + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: 'pnpm' + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run verification gate + run: pnpm verify +``` + +- [ ] **Step 5: Commit** + +```bash +git add .github/workflows/ci.yml package.json pnpm-lock.yaml +git commit -m "ci: add pnpm verify aggregate gate and GitHub Actions CI workflow" +``` + +--- + +### Task 13: Scaffold Documentation & Final Clean Verification + +**Files:** + +- Modify: `README.md` +- Delete: Any temporary or demo files not explicitly part of the specification + +**Interfaces:** + +- Consumes: Verified commands from repository +- Produces: Complete, accurate, concise `README.md` covering prerequisites, setup, scripts, loopback ports, database configuration, migrations, MCP invocation, architecture, and scaffold limitations. + +- [ ] **Step 1: Complete `README.md`** + +````markdown +# Relay + +Local task sidecar for human–AI workflows. + +> **Status:** Scaffold stage (Issue #1). Task tracking, companion skills, and agent features are deferred to subsequent issues. + +## Prerequisites + +- Node.js 24.x LTS (`.nvmrc`) +- pnpm 10.2.0 (managed via Corepack) + +## Setup + +```bash +corepack enable +pnpm install --frozen-lockfile +``` +```` + +## Available Scripts + +- `pnpm verify` — Non-mutating aggregate quality gate (runs format check, lint, typecheck, test coverage, build, asset validation, and audit). +- `pnpm format` — Format codebase with Prettier (mutating). +- `pnpm format:check` — Check formatting with Prettier. +- `pnpm lint` — Run ESLint (`--max-warnings=0`). +- `pnpm typecheck` — Perform strict TypeScript type checking. +- `pnpm test` — Run Vitest tests once. +- `pnpm test:coverage` — Run Vitest tests with V8 coverage enforcement. +- `pnpm build` — Build Node backend (`dist/mcp`, `dist/http`) and Vite web UI (`dist/web`). +- `pnpm dev:mcp` — Run MCP stdio entry point from source. +- `pnpm dev:http` — Run HTTP server from source (`http://127.0.0.1:43110`). +- `pnpm dev:web` — Run Vite development server with proxy to `/api`. +- `pnpm dev:ui` — Run HTTP server and Vite development server concurrently. +- `pnpm validate:assets` — Validate repository assets and configuration. + +## Configuration & Environment Variables + +- `RELAY_DB_PATH`: Custom path to SQLite database file. + - Default on Windows: `%APPDATA%\relay\relay.db` + - Default on macOS: `~/Library/Application Support/relay/relay.db` + - Default on Linux: `~/.local/share/relay/relay.db` +- `RELAY_HTTP_PORT`: Custom port for loopback HTTP server (default: `43110`). + +## Database & Migrations + +Relay uses `better-sqlite3` with plain SQL migrations located under `src/database/migrations/`. +On connection, SQLite PRAGMAs are executed: + +- `PRAGMA foreign_keys = ON;` +- `PRAGMA journal_mode = WAL;` +- `PRAGMA busy_timeout = 5000;` + +Applied SQL migrations are recorded with SHA-256 checksums in `_relay_migrations` and are immutable. + +## Local MCP Invocation + +After running `pnpm build`: + +```bash +node dist/mcp/main.js +``` + +Or invoke the package binary entry point: + +```bash +./dist/mcp/main.js +``` + +Exposes one scaffold tool: `relay_health`. + +## Architecture Boundaries + +```text +src/ + domain/ # Domain entities (deferred) + application/ # Application services (getHealth) + database/ # SQLite connection and migration runner + interfaces/ + mcp/ # MCP stdio server adapter + http/ # Loopback HTTP server adapter + shared/ # Errors and package metadata +web/ # Vite React 19 UI shell +``` + +- `domain` and `application` layers do not import interface or database code. +- MCP stdio process never writes non-protocol diagnostic data to `stdout`. +- HTTP server binds strictly to `127.0.0.1` (loopback). + +## Current Limitations + +- No task CRUD, task table, or product task behavior. +- No companion skills, plugin manifests, or vendor-specific MCP configs (moved to Issue #2). +- No remote access, authentication, multi-user accounts, or desktop packaging. + +```` + +- [ ] **Step 2: Run clean-checkout verification procedure** + +Run: +```bash +pnpm verify +```` + +- [ ] **Step 3: Verify git status is completely clean** + +Run: `git status --short` +Expected: Empty output. + +- [ ] **Step 4: Commit** + +```bash +git add README.md +git commit -m "docs: finalize scaffold README with setup instructions, architecture rules, and script references" +``` diff --git a/docs/superpowers/plans/2026-07-25-scaffold-review-fixes.md b/docs/superpowers/plans/2026-07-25-scaffold-review-fixes.md new file mode 100644 index 0000000..5ea3671 --- /dev/null +++ b/docs/superpowers/plans/2026-07-25-scaffold-review-fixes.md @@ -0,0 +1,95 @@ +# Scaffold Review Fixes Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix the issue `#1` scaffold gaps found in review without adding product scope. + +**Architecture:** Introduce one shared runtime-path module, extend the HTTP adapter with minimal production static serving, make MCP stdio shutdown graceful, and harden repository validation so `pnpm verify` enforces more of the actual scaffold contract. + +**Tech Stack:** TypeScript, Node.js `node:http`, `@modelcontextprotocol/sdk`, Vitest, tsx. + +## Global Constraints + +- Keep issue `#1` scaffold-only scope; do not add task features, agent skills, vendor configs, or auth. +- Preserve loopback-only HTTP behavior and stderr-only MCP diagnostics. +- Prefer shared helpers over duplicated path logic. +- Drive every behavior change with a failing test first. +- Keep `pnpm verify` non-mutating. + +--- + +### Task 1: Runtime Path Resolution + +**Files:** + +- Create: `src/shared/runtime-paths.ts` +- Modify: `src/shared/package-metadata.ts` +- Modify: `src/database/migrate.ts` +- Add/Modify Test: `tests/unit/shared/package-metadata.test.ts` + +**Interfaces:** + +- Produces: `getPackageRoot(): string`, `resolveFromPackageRoot(...segments: string[]): string` + +- [ ] Write a failing test proving package metadata still loads when the process working directory is outside the repo root. +- [ ] Implement the shared runtime-path helper by walking upward from the executing module location until `package.json` is found. +- [ ] Switch package metadata and default migration resolution to the new helper. +- [ ] Re-run the focused tests for package metadata and migrations. + +### Task 2: HTTP Production Static Serving + +**Files:** + +- Modify: `src/interfaces/http/create-http-server.ts` +- Add/Modify Test: `tests/integration/http-health.test.ts` + +**Interfaces:** + +- Produces: `GET /` serving `dist/web/index.html` when present, safe static file serving, stable JSON `404` fallback + +- [ ] Write a failing integration test that starts the HTTP server after `pnpm build:web` and expects `GET /` to return the built HTML shell. +- [ ] Implement minimal path-safe static serving from `dist/web`. +- [ ] Keep `/api/health` and `404`/`405` behavior intact. +- [ ] Re-run the focused HTTP tests. + +### Task 3: MCP Shutdown and Canonical Invocation + +**Files:** + +- Modify: `src/interfaces/mcp/main.ts` +- Add/Modify Test: `tests/integration/mcp-stdio.test.ts` + +**Interfaces:** + +- Produces: idempotent graceful stdio shutdown and proof that `relay-mcp` works from another working directory + +- [ ] Write a failing integration test that spawns the canonical built command from outside the repo root. +- [ ] Update the MCP entry point to close the stdio transport before exiting on `SIGINT`/`SIGTERM`. +- [ ] Re-run the focused MCP integration test. + +### Task 4: Repository Asset Validation + +**Files:** + +- Modify: `scripts/validate-repository-assets.ts` +- Add Test: `tests/unit/scripts/validate-repository-assets.test.ts` +- Modify: `README.md` only if needed to satisfy link validation + +**Interfaces:** + +- Produces: stricter validation for required files, parsed JSON, README local links, placeholder markers, and forbidden scope-creep assets + +- [ ] Write failing validator tests for broken local links and placeholder markers. +- [ ] Refactor the validator into testable functions and expand the checks. +- [ ] Re-run focused validator tests. + +### Task 5: Verification + +**Files:** + +- Modify: `docs/superpowers/specs/2026-07-25-scaffold-review-fixes-design.md` +- Modify: `docs/superpowers/plans/2026-07-25-scaffold-review-fixes.md` + +- [ ] Run the targeted tests touched by the fixes. +- [ ] Run `pnpm.cmd verify` for fresh end-to-end evidence. +- [ ] If audit is blocked by sandbox networking again, report that limitation explicitly with the passing local gates before audit. diff --git a/docs/superpowers/specs/2026-07-25-scaffold-review-fixes-design.md b/docs/superpowers/specs/2026-07-25-scaffold-review-fixes-design.md new file mode 100644 index 0000000..6cabe0c --- /dev/null +++ b/docs/superpowers/specs/2026-07-25-scaffold-review-fixes-design.md @@ -0,0 +1,64 @@ +# Scaffold Review Fixes Design + +**Context** + +Issue `#1` scaffold work is implemented, but the review identified four gaps against the spec: + +1. runtime path resolution depends on `process.cwd()` +2. the HTTP server does not serve the built web shell +3. MCP stdio shutdown exits abruptly instead of closing resources +4. repository asset validation is too narrow for the required quality gate + +**Goal** + +Close those gaps without widening scope beyond issue `#1`. The result should keep the scaffold minimal while making the built entry points relocatable, the loopback HTTP process production-usable, the MCP process cleanly stoppable, and `pnpm verify` more representative of the implementation spec. + +## Design + +### Runtime path resolution + +Add a shared runtime-path helper that derives the package root from the executing module location rather than `process.cwd()`. This keeps both source execution (`tsx`) and built execution (`dist/...`) working when launched from outside the repo root. + +Use that helper in: + +- package metadata loading +- default migrations directory resolution +- production web build directory resolution + +### HTTP production integration + +Keep `/api/health` as the application contract, then add minimal static serving for `dist/web` when present: + +- `GET /` serves `dist/web/index.html` +- safe in-tree file paths under the web build directory are served directly +- unknown routes still return the stable JSON `404` shape +- missing web build does not block API startup + +The implementation stays path-safe and loopback-only. + +### Clean shutdown + +Update the MCP stdio entry point to close the transport on `SIGINT`/`SIGTERM` before exiting. Shutdown should be idempotent and stderr-only. This matches current MCP SDK guidance for stdio serving and prevents future resource leaks once the process owns more state. + +### Asset validation + +Expand the validator so `pnpm verify` checks the scaffold more directly: + +- required scaffold files and directories +- `package.json#bin.relay-mcp` +- built MCP entry existence after build +- JSON example/config parseability +- README local Markdown links +- unresolved placeholder markers +- forbidden issue-`#1` scope creep assets such as `SKILL.md` and agent/vendor integration files + +## Testing + +Add regression tests for: + +- package metadata loading from a non-repo working directory +- built `relay-mcp` invocation from a non-repo working directory +- HTTP serving of built `index.html` +- validator failures for broken README links and placeholder content + +Existing HTTP, MCP, and verify coverage remains in place. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..151ab12 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,45 @@ +import tsParser from '@typescript-eslint/parser'; +import tsPlugin from '@typescript-eslint/eslint-plugin'; +import reactHooks from 'eslint-plugin-react-hooks'; + +/** @type {import('eslint').Linter.Config[]} */ +const config = [ + { + ignores: ['dist/**', 'coverage/**', 'node_modules/**'], + }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js'], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.test.json'], + }, + }, + plugins: { + '@typescript-eslint': tsPlugin, + 'react-hooks': reactHooks, + }, + rules: { + ...tsPlugin.configs['recommended'].rules, + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': 'error', + '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }], + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', + 'no-console': 'off', + }, + }, + { + files: ['src/interfaces/mcp/**/*.ts'], + rules: { + 'no-console': 'error', + }, + }, +]; + +export default config; diff --git a/package.json b/package.json new file mode 100644 index 0000000..108ac60 --- /dev/null +++ b/package.json @@ -0,0 +1,62 @@ +{ + "name": "relay", + "version": "0.1.0", + "description": "Local task sidecar for human–AI workflows", + "private": true, + "type": "module", + "engines": { + "node": ">=24 <25" + }, + "packageManager": "pnpm@10.2.0", + "bin": { + "relay-mcp": "./dist/mcp/main.js" + }, + "scripts": { + "test": "vitest run", + "test:coverage": "vitest run --coverage", + "lint": "eslint . --max-warnings=0", + "typecheck": "tsc --build --noEmit", + "format": "prettier --write .", + "format:check": "prettier --check .", + "build:node": "tsup", + "build:web": "vite build", + "build:clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"", + "build": "pnpm build:clean && pnpm build:node && pnpm build:web", + "dev:mcp": "node --import tsx/esm src/interfaces/mcp/main.ts", + "dev:http": "node --import tsx/esm src/interfaces/http/main.ts", + "dev:web": "vite", + "dev:ui": "concurrently -k -p name -c \"blue,green\" \"pnpm dev:http\" \"pnpm dev:web\"", + "validate:assets": "node --import tsx/esm scripts/validate-repository-assets.ts", + "audit": "pnpm audit --audit-level high", + "verify": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test:coverage && pnpm build && pnpm validate:assets && pnpm audit --audit-level high" + }, + "devDependencies": { + "@testing-library/jest-dom": "^7.0.0", + "@testing-library/react": "^16.3.2", + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^24.10.0", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@typescript-eslint/eslint-plugin": "^8.65.0", + "@typescript-eslint/parser": "^8.65.0", + "@vitejs/plugin-react": "^6.0.4", + "@vitest/coverage-v8": "^4.1.10", + "concurrently": "^10.0.4", + "eslint": "^10.8.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.3", + "jsdom": "^29.1.1", + "tsup": "^8.5.1", + "tsx": "^4.23.1", + "typescript": "^5.9.3", + "vite": "^8.1.5", + "vitest": "^4.1.10" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "better-sqlite3": "^13.0.1", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "zod": "^4.4.3" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..fb0cf1d --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,4420 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.29.0 + version: 1.29.0(zod@4.4.3) + better-sqlite3: + specifier: ^13.0.1 + version: 13.0.1 + react: + specifier: ^19.2.8 + version: 19.2.8 + react-dom: + specifier: ^19.2.8 + version: 19.2.8(react@19.2.8) + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + '@testing-library/jest-dom': + specifier: ^7.0.0 + version: 7.0.0(@testing-library/dom@10.4.1) + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@types/better-sqlite3': + specifier: ^7.6.13 + version: 7.6.13 + '@types/node': + specifier: ^26.1.1 + version: 26.1.1 + '@types/react': + specifier: ^19.2.17 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.17) + '@typescript-eslint/eslint-plugin': + specifier: ^8.65.0 + version: 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0)(typescript@5.9.3))(eslint@10.8.0)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.65.0 + version: 8.65.0(eslint@10.8.0)(typescript@5.9.3) + '@vitejs/plugin-react': + specifier: ^6.0.4 + version: 6.0.4(vite@8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(tsx@4.23.1)) + '@vitest/coverage-v8': + specifier: ^4.1.10 + version: 4.1.10(vitest@4.1.10) + concurrently: + specifier: ^10.0.4 + version: 10.0.4 + eslint: + specifier: ^10.8.0 + version: 10.8.0 + eslint-plugin-react-hooks: + specifier: ^7.1.1 + version: 7.1.1(eslint@10.8.0) + eslint-plugin-react-refresh: + specifier: ^0.5.3 + version: 0.5.3(eslint@10.8.0) + jsdom: + specifier: ^29.1.1 + version: 29.1.1 + tsup: + specifier: ^8.5.1 + version: 8.5.1(postcss@8.5.23)(tsx@4.23.1)(typescript@5.9.3) + tsx: + specifier: ^4.23.1 + version: 4.23.1 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^8.1.5 + version: 8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(tsx@4.23.1) + vitest: + specifier: ^4.1.10 + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(vite@8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(tsx@4.23.1)) + +packages: + + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@csstools/color-helpers@6.1.0': + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.3.0': + resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.10': + resolution: {integrity: sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.7': + resolution: {integrity: sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@hono/node-server@1.19.15': + resolution: {integrity: sha512-Za2ai6TLdKjUvnur+eenO6nuYYipVAEhyCAdaV8IRvmU9kK8crOZUSYvIXn72E4f8fJqyAbpcJuTsYYmZp9Deg==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@7.0.0': + resolution: {integrity: sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + peerDependencies: + '@testing-library/dom': '>=10 <11' + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/better-sqlite3@7.6.13': + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@26.1.1': + resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@typescript-eslint/eslint-plugin@8.65.0': + resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.65.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.65.0': + resolution: {integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.65.0': + resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.65.0': + resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.65.0': + resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.65.0': + resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.65.0': + resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.65.0': + resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.65.0': + resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.65.0': + resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@6.0.4': + resolution: {integrity: sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + '@vitest/coverage-v8@4.1.10': + resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} + peerDependencies: + '@vitest/browser': 4.1.10 + vitest: 4.1.10 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@1.0.5: + resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.11.1: + resolution: {integrity: sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==} + engines: {node: '>=6.0.0'} + hasBin: true + + better-sqlite3@13.0.1: + resolution: {integrity: sha512-LYpmOXdkpQYf4wmlxkdzW01XGlOXNIbjLg45yNkh0FQ4814VbK9PdOFmhZpYbej+EZtR/i3FDdhEG98HqZdgnA==} + engines: {node: '>=22'} + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + + brace-expansion@5.0.8: + resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + engines: {node: 20 || >=22} + + browserslist@4.28.7: + resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001806: + resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + concurrently@10.0.4: + resolution: {integrity: sha512-trZql+7l/0+WRAsAnEdctr4+iiOS6ZrViI6H8QWcCF9MFS/LT0dKpe8vluB1to6it+OxSI4VospFTIFMW8DJRw==} + engines: {node: '>=22'} + hasBin: true + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.396: + resolution: {integrity: sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react-refresh@0.5.3: + resolution: {integrity: sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==} + peerDependencies: + eslint: ^9 || ^10 + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.8.0: + resolution: {integrity: sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + express-rate-limit@8.6.0: + resolution: {integrity: sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.4: + resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.3: + resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + hono@4.12.32: + resolution: {integrity: sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==} + engines: {node: '>=16.9.0'} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.2.2: + resolution: {integrity: sha512-aNJVEVdXTr/g5+N2VCLo+CTrFLo/Y+9rx9RC5BpxPtf7NEknmzY+UQvMO8Fjni5KFmDaHJieL8HCMJKjJXsTgg==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jose@6.2.4: + resolution: {integrity: sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-addon-api@8.9.0: + resolution: {integrity: sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==} + engines: {node: ^18 || ^20 || >= 21} + + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + engines: {node: '>=18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + peerDependencies: + react: ^19.2.8 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.9.0: + resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tldts-core@7.4.9: + resolution: {integrity: sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==} + + tldts@7.4.9: + resolution: {integrity: sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==} + hasBin: true + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + tsx@4.23.1: + resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + engines: {node: '>=20.18.1'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite@8.1.5: + resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@adobe/css-tools@4.5.0': {} + + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.7 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@csstools/color-helpers@6.1.0': {} + + '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.0 + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.7(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0)': + dependencies: + eslint: 10.8.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.7.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@exodus/bytes@1.15.1': {} + + '@hono/node-server@1.19.15(hono@4.12.32)': + dependencies: + hono: 4.12.32 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': + dependencies: + '@hono/node-server': 1.19.15(hono@4.12.32) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.0 + express: 5.2.1 + express-rate-limit: 8.6.0(express@5.2.1) + hono: 4.12.32 + jose: 6.2.4 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-project/types@0.139.0': {} + + '@rolldown/binding-android-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-x64@1.1.5': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.5': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.5': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.5': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.5': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.5': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@standard-schema/spec@1.1.0': {} + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@7.0.0(@testing-library/dom@10.4.1)': + dependencies: + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.1 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/better-sqlite3@7.6.13': + dependencies: + '@types/node': 26.1.1 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@26.1.1': + dependencies: + undici-types: 8.3.0 + + '@types/react-dom@19.2.3(@types/react@19.2.17)': + dependencies: + '@types/react': 19.2.17 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0)(typescript@5.9.3))(eslint@10.8.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.65.0(eslint@10.8.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/type-utils': 8.65.0(eslint@10.8.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.8.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.65.0 + eslint: 10.8.0 + ignore: 7.0.6 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.65.0(eslint@10.8.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.65.0 + debug: 4.4.3 + eslint: 10.8.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.65.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3) + '@typescript-eslint/types': 8.65.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.65.0': + dependencies: + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 + + '@typescript-eslint/tsconfig-utils@8.65.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.65.0(eslint@10.8.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.8.0)(typescript@5.9.3) + debug: 4.4.3 + eslint: 10.8.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.65.0': {} + + '@typescript-eslint/typescript-estree@8.65.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.65.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3) + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.65.0(eslint@10.8.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) + eslint: 10.8.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.65.0': + dependencies: + '@typescript-eslint/types': 8.65.0 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-react@6.0.4(vite@8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(tsx@4.23.1))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(tsx@4.23.1) + + '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.10 + ast-v8-to-istanbul: 1.0.5 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.3 + obug: 2.1.4 + std-env: 4.2.0 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(vite@8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(tsx@4.23.1)) + + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(tsx@4.23.1))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(tsx@4.23.1) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.4 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + any-promise@1.3.0: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@1.0.5: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.11.1: {} + + better-sqlite3@13.0.1: + dependencies: + node-addon-api: 8.9.0 + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.0.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + brace-expansion@5.0.8: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.7: + dependencies: + baseline-browser-mapping: 2.11.1 + caniuse-lite: 1.0.30001806 + electron-to-chromium: 1.5.396 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.7) + + bundle-require@5.1.0(esbuild@0.27.7): + dependencies: + esbuild: 0.27.7 + load-tsconfig: 0.2.5 + + bytes@3.1.2: {} + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caniuse-lite@1.0.30001806: {} + + chai@6.2.2: {} + + chalk@5.6.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + commander@4.1.1: {} + + concurrently@10.0.4: + dependencies: + chalk: 5.6.2 + rxjs: 7.8.2 + shell-quote: 1.9.0 + supports-color: 10.2.2 + tree-kill: 1.2.2 + yargs: 18.0.0 + + confbox@0.1.8: {} + + consola@3.4.2: {} + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + + csstype@3.2.3: {} + + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-is@0.1.4: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.396: {} + + emoji-regex@10.6.0: {} + + encodeurl@2.0.0: {} + + entities@8.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.3.1: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@7.1.1(eslint@10.8.0): + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + eslint: 10.8.0 + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.5.3(eslint@10.8.0): + dependencies: + eslint: 10.8.0 + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.8.0: + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.7.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + etag@1.8.1: {} + + eventsource-parser@3.1.0: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.1.0 + + expect-type@1.4.0: {} + + express-rate-limit@8.6.0(express@5.2.1): + dependencies: + debug: 4.4.3 + express: 5.2.1 + ip-address: 10.2.2 + transitivePeerDependencies: + - supports-color + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.3.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.4: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.2 + rollup: 4.62.2 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.3 + keyv: 4.5.4 + + flatted@3.4.3: {} + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + hono@4.12.32: {} + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + + html-escaper@2.0.2: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.6: {} + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + inherits@2.0.4: {} + + ip-address@10.2.2: {} + + ipaddr.js@1.9.1: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-potential-custom-element-name@1.0.1: {} + + is-promise@4.0.0: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jose@6.2.4: {} + + joycon@3.1.1: {} + + js-tokens@10.0.0: {} + + js-tokens@4.0.0: {} + + jsdom@29.1.1: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.7(css-tree@3.2.1) + '@exodus/bytes': 1.15.1 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + undici: 7.29.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lru-cache@11.5.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.3: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.5 + + math-intrinsics@1.1.0: {} + + mdn-data@2.27.1: {} + + media-typer@1.1.1: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + min-indent@1.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.8 + + mlly@1.8.2: + dependencies: + acorn: 8.17.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.16: {} + + natural-compare@1.4.0: {} + + negotiator@1.0.0: {} + + node-addon-api@8.9.0: {} + + node-releases@2.0.51: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + obug@2.1.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parse5@8.0.1: + dependencies: + entities: 8.0.0 + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-to-regexp@8.4.2: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + pirates@4.0.7: {} + + pkce-challenge@5.0.1: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + postcss-load-config@6.0.1(postcss@8.5.23)(tsx@4.23.1): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.23 + tsx: 4.23.1 + + postcss@8.5.23: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + punycode@2.3.1: {} + + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react-is@17.0.2: {} + + react@19.2.8: {} + + readdirp@4.1.2: {} + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + require-from-string@2.0.2: {} + + resolve-from@5.0.0: {} + + rolldown@1.1.5: + dependencies: + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.9.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + source-map@0.7.6: {} + + stackback@0.0.2: {} + + statuses@2.0.2: {} + + std-env@4.2.0: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.17 + ts-interface-checker: 0.1.13 + + supports-color@10.2.2: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + symbol-tree@3.2.4: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinyrainbow@3.1.0: {} + + tldts-core@7.4.9: {} + + tldts@7.4.9: + dependencies: + tldts-core: 7.4.9 + + toidentifier@1.0.1: {} + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.9 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + tree-kill@1.2.2: {} + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-interface-checker@0.1.13: {} + + tslib@2.8.1: {} + + tsup@8.5.1(postcss@8.5.23)(tsx@4.23.1)(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.7) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.7 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.23)(tsx@4.23.1) + resolve-from: 5.0.0 + rollup: 4.62.2 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.23 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsx@4.23.1: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + + typescript@5.9.3: {} + + ufo@1.6.4: {} + + undici-types@8.3.0: {} + + undici@7.29.0: {} + + unpipe@1.0.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.7): + dependencies: + browserslist: 4.28.7 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vary@1.1.2: {} + + vite@8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(tsx@4.23.1): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.23 + rolldown: 1.1.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.1.1 + esbuild: 0.27.7 + fsevents: 2.3.3 + tsx: 4.23.1 + + vitest@4.1.10(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(vite@8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(tsx@4.23.1)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(tsx@4.23.1)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(tsx@4.23.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.1.1 + '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) + jsdom: 29.1.1 + transitivePeerDependencies: + - msw + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@22.0.0: {} + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + yocto-queue@0.1.0: {} + + zod-to-json-schema@3.25.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} diff --git a/scripts/validate-repository-assets.ts b/scripts/validate-repository-assets.ts new file mode 100644 index 0000000..1c6b497 --- /dev/null +++ b/scripts/validate-repository-assets.ts @@ -0,0 +1,180 @@ +import { existsSync, readFileSync, readdirSync } from 'node:fs'; +import { isAbsolute, join, relative, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +function fail(msg: string): never { + throw new Error(`[ASSET VALIDATION FAILURE] ${msg}`); +} + +export interface ValidateRepositoryAssetsOptions { + readonly rootDir?: string; +} + +function walkFiles(rootDir: string, startDir = rootDir): string[] { + const entries = readdirSync(startDir, { withFileTypes: true }); + const files: string[] = []; + + for (const entry of entries) { + if (['.git', 'node_modules', 'dist', 'coverage'].includes(entry.name)) { + continue; + } + + const fullPath = join(startDir, entry.name); + if (entry.isDirectory()) { + files.push(...walkFiles(rootDir, fullPath)); + continue; + } + + files.push(fullPath); + } + + return files; +} + +function validateMarkdownLinks(markdownPath: string, content: string): void { + const linkPattern = /\[[^\]]+\]\(([^)]+)\)/g; + + for (const match of content.matchAll(linkPattern)) { + const rawTarget = match[1]; + if (!rawTarget) { + continue; + } + + if ( + rawTarget.startsWith('http://') || + rawTarget.startsWith('https://') || + rawTarget.startsWith('mailto:') || + rawTarget.startsWith('#') + ) { + continue; + } + + const cleanTarget = rawTarget.split('#')[0]?.split('?')[0]; + if (!cleanTarget) { + continue; + } + + const resolvedTarget = isAbsolute(cleanTarget) + ? cleanTarget + : resolve(markdownPath, '..', cleanTarget); + + if (!existsSync(resolvedTarget)) { + fail(`README local link does not resolve: ${rawTarget}`); + } + } +} + +function validateJsonFiles(files: readonly string[]): void { + for (const filePath of files) { + if (!filePath.endsWith('.json')) { + continue; + } + + JSON.parse(readFileSync(filePath, 'utf-8')); + } +} + +function validatePlaceholders(files: readonly string[]): void { + const placeholderTokens = ['TO' + 'DO', 'TB' + 'D']; + const placeholderPattern = new RegExp(`\\b(?:${placeholderTokens.join('|')})\\b`); + const textExtensions = new Set([ + '.md', + '.ts', + '.tsx', + '.js', + '.json', + '.yml', + '.yaml', + '.sql', + '.html', + '.css', + '.mjs', + '.cjs', + ]); + + for (const filePath of files) { + const extension = filePath.slice(filePath.lastIndexOf('.')); + if (!textExtensions.has(extension)) { + continue; + } + + const content = readFileSync(filePath, 'utf-8'); + const match = content.match(placeholderPattern); + if (match) { + fail( + `Unresolved placeholder marker ${match[0]} found in ${relative(process.cwd(), filePath) || filePath}`, + ); + } + } +} + +export function validateRepositoryAssets(options: ValidateRepositoryAssetsOptions = {}): void { + const rootDir = options.rootDir ? resolve(options.rootDir) : process.cwd(); + + // 1. Required files/directories + const requiredPaths = [ + '.nvmrc', + '.editorconfig', + '.gitignore', + '.prettierrc.json', + 'eslint.config.js', + 'package.json', + 'README.md', + 'tsconfig.base.json', + 'src/application/health/get-health.ts', + 'src/database/connection.ts', + 'src/interfaces/mcp/create-mcp-server.ts', + 'src/interfaces/http/create-http-server.ts', + 'web/src/App.tsx', + ]; + + for (const p of requiredPaths) { + if (!existsSync(join(rootDir, p))) { + fail(`Required path missing: ${p}`); + } + } + + // 2. package.json bin validation + const pkg = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf-8')) as { + bin?: Record; + }; + const binRelayMcp = pkg.bin?.['relay-mcp']; + if (binRelayMcp !== './dist/mcp/main.js') { + fail( + `package.json#bin.relay-mcp must point to ./dist/mcp/main.js (got ${String(binRelayMcp)})`, + ); + } + + // 3. Built MCP file existence after build + if (!existsSync(join(rootDir, 'dist', 'mcp', 'main.js'))) { + fail('Built MCP executable missing at dist/mcp/main.js. Run pnpm build first.'); + } + + const allFiles = walkFiles(rootDir); + + validateJsonFiles(allFiles); + validatePlaceholders(allFiles); + validateMarkdownLinks( + join(rootDir, 'README.md'), + readFileSync(join(rootDir, 'README.md'), 'utf-8'), + ); + + // 4. No SKILL.md or agent configs in #1 + const forbidden = ['SKILL.md', 'agent/skills', 'agent/mcp']; + for (const f of forbidden) { + if (existsSync(join(rootDir, f))) { + fail(`Forbidden asset for Issue #1 present: ${f}`); + } + } +} + +if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { + try { + validateRepositoryAssets(); + process.stdout.write('Repository asset validation passed successfully.\n'); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`${message}\n`); + process.exit(1); + } +} diff --git a/src/application/health/get-health.ts b/src/application/health/get-health.ts new file mode 100644 index 0000000..0ed7451 --- /dev/null +++ b/src/application/health/get-health.ts @@ -0,0 +1,11 @@ +import type { HealthStatus } from './health.js'; +import { getPackageMetadata } from '../../shared/package-metadata.js'; + +export function getHealth(): HealthStatus { + const meta = getPackageMetadata(); + return { + name: 'relay', + status: 'ok', + version: meta.version, + }; +} diff --git a/src/application/health/health.ts b/src/application/health/health.ts new file mode 100644 index 0000000..9ae3752 --- /dev/null +++ b/src/application/health/health.ts @@ -0,0 +1,5 @@ +export interface HealthStatus { + readonly name: 'relay'; + readonly status: 'ok'; + readonly version: string; +} diff --git a/src/database/connection.ts b/src/database/connection.ts new file mode 100644 index 0000000..76c8588 --- /dev/null +++ b/src/database/connection.ts @@ -0,0 +1,32 @@ +import Database from 'better-sqlite3'; +import { mkdirSync } from 'node:fs'; +import { dirname } from 'node:path'; +import { RelayError } from '../shared/errors.js'; +import { resolveDatabasePath } from './database-config.js'; + +export interface DatabaseConnectionOptions { + readonly path?: string; + readonly readonly?: boolean; +} + +export function createDatabaseConnection( + options: DatabaseConnectionOptions = {}, +): Database.Database { + const dbPath = resolveDatabasePath(options.path); + + if (dbPath !== ':memory:') { + mkdirSync(dirname(dbPath), { recursive: true }); + } + + const db = new Database(dbPath, { readonly: options.readonly ?? false }); + + db.pragma('foreign_keys = ON'); + const journalMode = db.pragma('journal_mode = WAL', { simple: true }); + if (!db.readonly && dbPath !== ':memory:' && String(journalMode).toLowerCase() !== 'wal') { + db.close(); + throw new RelayError(`Failed to enable WAL journal mode for database at ${dbPath}.`); + } + db.pragma('busy_timeout = 5000'); + + return db; +} diff --git a/src/database/database-config.ts b/src/database/database-config.ts new file mode 100644 index 0000000..400cddb --- /dev/null +++ b/src/database/database-config.ts @@ -0,0 +1,32 @@ +import { join } from 'node:path'; +import { homedir } from 'node:os'; +import { RelayError } from '../shared/errors.js'; + +export function getDefaultDatabasePath(): string { + const home = homedir(); + if (process.platform === 'win32') { + const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming'); + return join(appData, 'relay', 'relay.db'); + } + if (process.platform === 'darwin') { + return join(home, 'Library', 'Application Support', 'relay', 'relay.db'); + } + const xdgData = process.env.XDG_DATA_HOME || join(home, '.local', 'share'); + return join(xdgData, 'relay', 'relay.db'); +} + +export function resolveDatabasePath(explicitPath?: string): string { + if (explicitPath !== undefined) { + if (!explicitPath.trim()) { + throw new RelayError('Database path cannot be empty or whitespace only.'); + } + return explicitPath.trim(); + } + + const envPath = process.env.RELAY_DB_PATH; + if (envPath && envPath.trim()) { + return envPath.trim(); + } + + return getDefaultDatabasePath(); +} diff --git a/src/database/migrate.ts b/src/database/migrate.ts new file mode 100644 index 0000000..747e353 --- /dev/null +++ b/src/database/migrate.ts @@ -0,0 +1,67 @@ +import type Database from 'better-sqlite3'; +import { loadMigrationFiles } from './migration.js'; +import { RelayError } from '../shared/errors.js'; +import { resolveFromPackageRoot } from '../shared/runtime-paths.js'; + +export interface MigrationOptions { + readonly migrationsDir?: string; +} + +export function runMigrations(db: Database.Database, options: MigrationOptions = {}): void { + const migrationsDir = + options.migrationsDir || resolveFromPackageRoot('src', 'database', 'migrations'); + + db.exec(` + CREATE TABLE IF NOT EXISTS _relay_migrations ( + version INTEGER PRIMARY KEY, + name TEXT NOT NULL, + checksum TEXT NOT NULL, + applied_at TEXT NOT NULL + ); + `); + + const appliedRows = db + .prepare('SELECT version, name, checksum FROM _relay_migrations ORDER BY version ASC') + .all() as { + version: number; + name: string; + checksum: string; + }[]; + + const appliedMap = new Map(appliedRows.map((r) => [r.version, r])); + const migrationFiles = loadMigrationFiles(migrationsDir); + const availableVersions = new Set(migrationFiles.map((file) => file.version)); + + for (const applied of appliedRows) { + if (availableVersions.has(applied.version)) { + continue; + } + + throw new RelayError( + `Migration mismatch for version ${applied.version} (${String(applied.version).padStart(4, '0')}_${applied.name}.sql). ` + + `Applied checksum/name does not match repository SQL file. Applied SQL files are immutable.`, + ); + } + + for (const file of migrationFiles) { + const applied = appliedMap.get(file.version); + if (applied) { + if (applied.checksum !== file.checksum || applied.name !== file.name) { + throw new RelayError( + `Migration mismatch for version ${file.version} (${file.filename}). ` + + `Applied checksum/name does not match repository SQL file. Applied SQL files are immutable.`, + ); + } + continue; + } + + const applyTransaction = db.transaction(() => { + db.exec(file.sql); + db.prepare( + `INSERT INTO _relay_migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)`, + ).run(file.version, file.name, file.checksum); + }); + + applyTransaction(); + } +} diff --git a/src/database/migration.ts b/src/database/migration.ts new file mode 100644 index 0000000..5f6bd23 --- /dev/null +++ b/src/database/migration.ts @@ -0,0 +1,51 @@ +import { createHash } from 'node:crypto'; +import { readdirSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { RelayError } from '../shared/errors.js'; + +export interface MigrationFile { + readonly version: number; + readonly name: string; + readonly filename: string; + readonly sql: string; + readonly checksum: string; +} + +export function computeChecksum(content: string): string { + return createHash('sha256').update(content, 'utf-8').digest('hex'); +} + +export function loadMigrationFiles(migrationsDir: string): readonly MigrationFile[] { + const entries = readdirSync(migrationsDir, { withFileTypes: true }); + const files: MigrationFile[] = []; + const seenVersions = new Set(); + + for (const entry of entries) { + if (!entry.isFile() || !entry.name.endsWith('.sql')) continue; + + const match = /^(\d{4})_(.+)\.sql$/.exec(entry.name); + if (!match) { + throw new RelayError( + `Malformed migration filename: ${entry.name}. Expected format: NNNN_description.sql`, + ); + } + + const versionStr = match[1]; + const name = match[2]; + if (!versionStr || !name) continue; + + const version = parseInt(versionStr, 10); + if (seenVersions.has(version)) { + throw new RelayError(`Duplicate migration version prefix: ${versionStr}`); + } + seenVersions.add(version); + + const fullPath = join(migrationsDir, entry.name); + const sql = readFileSync(fullPath, 'utf-8'); + const checksum = computeChecksum(sql); + + files.push({ version, name, filename: entry.name, sql, checksum }); + } + + return files.sort((a, b) => a.version - b.version); +} diff --git a/src/database/migrations/0001_scaffold.sql b/src/database/migrations/0001_scaffold.sql new file mode 100644 index 0000000..bfe2b84 --- /dev/null +++ b/src/database/migrations/0001_scaffold.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS relay_metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +INSERT OR IGNORE INTO relay_metadata (key, value) VALUES ('schema_version', '1'); diff --git a/src/domain/README.md b/src/domain/README.md new file mode 100644 index 0000000..21619b9 --- /dev/null +++ b/src/domain/README.md @@ -0,0 +1,5 @@ +# Domain Layer + +Domain models, entities, and business rules for Relay. + +> **Note:** Domain task lifecycle rules and entities are intentionally deferred to subsequent issues (Issue #2+). diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..7e3a61c --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/interfaces/http/create-http-server.ts b/src/interfaces/http/create-http-server.ts new file mode 100644 index 0000000..c6f4e27 --- /dev/null +++ b/src/interfaces/http/create-http-server.ts @@ -0,0 +1,167 @@ +import { createServer, type IncomingMessage, type ServerResponse, type Server } from 'node:http'; +import { existsSync, readFileSync, statSync } from 'node:fs'; +import { extname, relative, resolve } from 'node:path'; +import { getHealth } from '../../application/health/get-health.js'; +import { RelayError } from '../../shared/errors.js'; +import { resolveFromPackageRoot } from '../../shared/runtime-paths.js'; + +export interface HttpServerOptions { + readonly host?: string; + readonly port?: number; +} + +export interface HttpServerInstance { + readonly server: Server; + readonly host: string; + readonly port: number; + readonly url: string; + readonly stop: () => Promise; +} + +const webBuildDirectory = resolveFromPackageRoot('dist', 'web'); + +export function getContentType(filePath: string): string { + switch (extname(filePath)) { + case '.html': + return 'text/html; charset=utf-8'; + case '.js': + return 'text/javascript; charset=utf-8'; + case '.css': + return 'text/css; charset=utf-8'; + case '.json': + return 'application/json; charset=utf-8'; + case '.svg': + return 'image/svg+xml'; + case '.ico': + return 'image/x-icon'; + default: + return 'application/octet-stream'; + } +} + +export function resolveStaticAsset(pathname: string): string | null { + if (!existsSync(webBuildDirectory)) { + return null; + } + + const relativePath = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, ''); + if (!relativePath) { + return null; + } + + const candidatePath = resolve(webBuildDirectory, relativePath); + const relativeToBuildDir = relative(webBuildDirectory, candidatePath); + + if ( + relativeToBuildDir.startsWith('..') || + relativeToBuildDir.includes('..\\') || + relativeToBuildDir.includes('../') + ) { + return null; + } + + if (!existsSync(candidatePath) || !statSync(candidatePath).isFile()) { + return null; + } + + return candidatePath; +} + +export function resolveHttpPort(explicitPort?: number): number { + if (explicitPort !== undefined) { + if (explicitPort < 0 || explicitPort > 65535) { + throw new RelayError(`Invalid HTTP port: ${explicitPort}. Must be between 0 and 65535.`); + } + return explicitPort; + } + + const envPort = process.env.RELAY_HTTP_PORT; + if (envPort) { + const parsed = parseInt(envPort, 10); + if (isNaN(parsed) || parsed < 1 || parsed > 65535) { + throw new RelayError(`Invalid RELAY_HTTP_PORT environment variable: ${envPort}.`); + } + return parsed; + } + + return 43110; +} + +export function createHttpServer(options: HttpServerOptions = {}): Promise { + const host = options.host || '127.0.0.1'; + let port: number; + + try { + port = resolveHttpPort(options.port); + if (host !== '127.0.0.1' && host !== 'localhost') { + throw new RelayError( + `Loopback security restriction: HTTP server host must be 127.0.0.1 or localhost (got ${host}).`, + ); + } + } catch (err) { + return Promise.reject(err); + } + + const requestHandler = (req: IncomingMessage, res: ServerResponse) => { + const url = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`); + + if (url.pathname === '/api/health') { + if (req.method !== 'GET') { + res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8', Allow: 'GET' }); + res.end(JSON.stringify({ error: 'method_not_allowed' })); + return; + } + + const health = getHealth(); + res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); + res.end(JSON.stringify(health)); + return; + } + + if (req.method === 'GET' || req.method === 'HEAD') { + const staticAssetPath = resolveStaticAsset(url.pathname); + if (staticAssetPath) { + res.writeHead(200, { 'Content-Type': getContentType(staticAssetPath) }); + if (req.method === 'HEAD') { + res.end(); + return; + } + + res.end(readFileSync(staticAssetPath)); + return; + } + } + + res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' }); + res.end(JSON.stringify({ error: 'not_found' })); + }; + + const server = createServer(requestHandler); + + return new Promise((resolve, reject) => { + server.on('error', (err) => reject(new RelayError('HTTP server error', err))); + + server.listen(port, host, () => { + const addr = server.address(); + const actualPort = typeof addr === 'object' && addr ? addr.port : port; + const serverUrl = `http://${host}:${actualPort}`; + + const stop = (): Promise => { + return new Promise((resStop, rejStop) => { + server.close((err) => { + if (err) rejStop(err); + else resStop(); + }); + }); + }; + + resolve({ + server, + host, + port: actualPort, + url: serverUrl, + stop, + }); + }); + }); +} diff --git a/src/interfaces/http/main.ts b/src/interfaces/http/main.ts new file mode 100644 index 0000000..eebbbb0 --- /dev/null +++ b/src/interfaces/http/main.ts @@ -0,0 +1,29 @@ +import { createHttpServer } from './create-http-server.js'; + +async function main(): Promise { + try { + const instance = await createHttpServer(); + process.stderr.write(`[INFO] HTTP server running at ${instance.url}\n`); + + const shutdown = () => { + process.stderr.write('[INFO] Stopping HTTP server...\n'); + void instance + .stop() + .then(() => process.exit(0)) + .catch((error: unknown) => { + const msg = error instanceof Error ? error.message : String(error); + process.stderr.write(`[ERROR] Failed to stop HTTP server cleanly: ${msg}\n`); + process.exit(1); + }); + }; + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + process.stderr.write(`[ERROR] Fatal HTTP server error: ${msg}\n`); + process.exit(1); + } +} + +void main(); diff --git a/src/interfaces/mcp/create-mcp-server.ts b/src/interfaces/mcp/create-mcp-server.ts new file mode 100644 index 0000000..d0244b1 --- /dev/null +++ b/src/interfaces/mcp/create-mcp-server.ts @@ -0,0 +1,25 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { getHealth } from '../../application/health/get-health.js'; +import { getPackageMetadata } from '../../shared/package-metadata.js'; + +export function createMcpServer(): McpServer { + const meta = getPackageMetadata(); + const server = new McpServer({ + name: meta.name, + version: meta.version, + }); + + server.tool('relay_health', 'Return health status of the local Relay service', {}, async () => { + const health = getHealth(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(health), + }, + ], + }; + }); + + return server; +} diff --git a/src/interfaces/mcp/logger.ts b/src/interfaces/mcp/logger.ts new file mode 100644 index 0000000..634577f --- /dev/null +++ b/src/interfaces/mcp/logger.ts @@ -0,0 +1,9 @@ +export const mcpLogger = { + info(message: string): void { + process.stderr.write(`[INFO] ${message}\n`); + }, + error(message: string, error?: unknown): void { + const detail = error instanceof Error ? `: ${error.message}` : ''; + process.stderr.write(`[ERROR] ${message}${detail}\n`); + }, +}; diff --git a/src/interfaces/mcp/main.ts b/src/interfaces/mcp/main.ts new file mode 100644 index 0000000..c94d96b --- /dev/null +++ b/src/interfaces/mcp/main.ts @@ -0,0 +1,43 @@ +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { createMcpServer } from './create-mcp-server.js'; +import { mcpLogger } from './logger.js'; + +async function main(): Promise { + try { + const server = createMcpServer(); + const transport = new StdioServerTransport(); + let shuttingDown = false; + + const shutdown = async (signal: 'SIGINT' | 'SIGTERM'): Promise => { + if (shuttingDown) { + return; + } + + shuttingDown = true; + mcpLogger.info(`Received ${signal}, shutting down MCP server...`); + + try { + await server.close(); + process.exitCode = 0; + } catch (error) { + mcpLogger.error('Failed during MCP shutdown', error); + process.exitCode = 1; + } + }; + + process.on('SIGINT', () => { + void shutdown('SIGINT'); + }); + + process.on('SIGTERM', () => { + void shutdown('SIGTERM'); + }); + + await server.connect(transport); + } catch (error) { + mcpLogger.error('Fatal error starting MCP stdio server', error); + process.exit(1); + } +} + +void main(); diff --git a/src/shared/errors.ts b/src/shared/errors.ts new file mode 100644 index 0000000..070c956 --- /dev/null +++ b/src/shared/errors.ts @@ -0,0 +1,9 @@ +export class RelayError extends Error { + constructor( + message: string, + override readonly cause?: unknown, + ) { + super(message); + this.name = 'RelayError'; + } +} diff --git a/src/shared/package-metadata.ts b/src/shared/package-metadata.ts new file mode 100644 index 0000000..4473600 --- /dev/null +++ b/src/shared/package-metadata.ts @@ -0,0 +1,23 @@ +import { readFileSync } from 'node:fs'; +import { resolveFromPackageRoot } from './runtime-paths.js'; + +export interface PackageMetadata { + readonly name: string; + readonly version: string; +} + +let cachedMetadata: PackageMetadata | null = null; + +export function getPackageMetadata(): PackageMetadata { + if (cachedMetadata) return cachedMetadata; + + const pkgPath = resolveFromPackageRoot('package.json'); + const content = readFileSync(pkgPath, 'utf-8'); + const parsed = JSON.parse(content) as { name: string; version: string }; + + cachedMetadata = { + name: parsed.name, + version: parsed.version, + }; + return cachedMetadata; +} diff --git a/src/shared/runtime-paths.ts b/src/shared/runtime-paths.ts new file mode 100644 index 0000000..c94e2af --- /dev/null +++ b/src/shared/runtime-paths.ts @@ -0,0 +1,34 @@ +import { existsSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { RelayError } from './errors.js'; + +const moduleDirectory = dirname(fileURLToPath(import.meta.url)); + +let cachedPackageRoot: string | null = null; + +function findPackageRoot(startDirectory: string): string { + let currentDirectory = resolve(startDirectory); + + for (;;) { + if (existsSync(join(currentDirectory, 'package.json'))) { + return currentDirectory; + } + + const parentDirectory = dirname(currentDirectory); + if (parentDirectory === currentDirectory) { + throw new RelayError(`Unable to locate package root from ${startDirectory}.`); + } + + currentDirectory = parentDirectory; + } +} + +export function getPackageRoot(): string { + cachedPackageRoot ??= findPackageRoot(moduleDirectory); + return cachedPackageRoot; +} + +export function resolveFromPackageRoot(...segments: string[]): string { + return join(getPackageRoot(), ...segments); +} diff --git a/tests/integration/database-migrations.test.ts b/tests/integration/database-migrations.test.ts new file mode 100644 index 0000000..07905c8 --- /dev/null +++ b/tests/integration/database-migrations.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { rmSync, writeFileSync, mkdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { createTemporaryDatabase } from '../support/temporary-database.js'; +import { runMigrations } from '../../src/database/migrate.js'; +import { RelayError } from '../../src/shared/errors.js'; + +describe('database-migrations integration', () => { + let tempDb: ReturnType | null = null; + + afterEach(() => { + tempDb?.cleanup(); + tempDb = null; + }); + + it('runs migrations on a fresh temporary SQLite database and verifies PRAGMAs', () => { + tempDb = createTemporaryDatabase(); + const { db } = tempDb; + + runMigrations(db); + + const fk = db.pragma('foreign_keys', { simple: true }); + const jm = db.pragma('journal_mode', { simple: true }); + const bt = db.pragma('busy_timeout', { simple: true }); + + expect(fk).toBe(1); + expect(jm).toBe('wal'); + expect(bt).toBe(5000); + + const migrations = db.prepare('SELECT version, name FROM _relay_migrations').all() as { + version: number; + name: string; + }[]; + expect(migrations).toHaveLength(1); + expect(migrations[0]?.version).toBe(1); + expect(migrations[0]?.name).toBe('scaffold'); + + // Verify relay_metadata table scaffolded by 0001_scaffold.sql + const meta = db + .prepare('SELECT key, value FROM relay_metadata WHERE key = ?') + .get('schema_version') as { + key: string; + value: string; + }; + expect(meta).toEqual({ key: 'schema_version', value: '1' }); + + // Idempotence test + expect(() => runMigrations(db)).not.toThrow(); + }); + + it('detects migration file tampering and throws RelayError', () => { + tempDb = createTemporaryDatabase(); + const { db, dir } = tempDb; + + // Custom migrations directory inside temp dir + const migrationsDir = join(dir, 'migrations'); + mkdirSync(migrationsDir, { recursive: true }); + + const sqlPath = join(migrationsDir, '0001_initial.sql'); + writeFileSync(sqlPath, 'CREATE TABLE test_table (id INT PRIMARY KEY);'); + + // Run first time + runMigrations(db, { migrationsDir }); + + // Tamper with the migration SQL file + writeFileSync(sqlPath, 'CREATE TABLE test_table (id INT PRIMARY KEY, name TEXT);'); + + // Attempting re-migration must fail due to SHA-256 mismatch + expect(() => runMigrations(db, { migrationsDir })).toThrow(RelayError); + expect(() => runMigrations(db, { migrationsDir })).toThrow(/Migration mismatch/); + }); + + it('rejects startup when an already-applied migration file is missing', () => { + tempDb = createTemporaryDatabase(); + const { db, dir } = tempDb; + + const migrationsDir = join(dir, 'migrations'); + mkdirSync(migrationsDir, { recursive: true }); + + const sqlPath = join(migrationsDir, '0001_initial.sql'); + writeFileSync(sqlPath, 'CREATE TABLE test_table (id INT PRIMARY KEY);'); + + runMigrations(db, { migrationsDir }); + rmSync(sqlPath); + + expect(() => runMigrations(db, { migrationsDir })).toThrow(RelayError); + expect(() => runMigrations(db, { migrationsDir })).toThrow(/Migration mismatch/); + }); +}); diff --git a/tests/integration/http-health.test.ts b/tests/integration/http-health.test.ts new file mode 100644 index 0000000..484092b --- /dev/null +++ b/tests/integration/http-health.test.ts @@ -0,0 +1,81 @@ +import { execSync } from 'node:child_process'; +import { afterEach, beforeAll, describe, expect, it } from 'vitest'; +import { + createHttpServer, + type HttpServerInstance, +} from '../../src/interfaces/http/create-http-server.js'; + +describe('http-health integration', () => { + let serverInstance: HttpServerInstance | null = null; + + beforeAll(() => { + execSync('pnpm build:web', { stdio: 'inherit' }); + }); + + afterEach(async () => { + if (serverInstance) { + await serverInstance.stop(); + serverInstance = null; + } + }); + + it('starts on 127.0.0.1 and returns 200 for GET /api/health', async () => { + serverInstance = await createHttpServer({ host: '127.0.0.1', port: 0 }); + const { url } = serverInstance; + + const res = await fetch(`${url}/api/health`); + expect(res.status).toBe(200); + expect(res.headers.get('content-type')).toContain('application/json'); + + const body = (await res.json()) as { name: string; status: string; version: string }; + expect(body).toEqual({ name: 'relay', status: 'ok', version: '0.1.0' }); + }); + + it('returns 405 Method Not Allowed for POST /api/health', async () => { + serverInstance = await createHttpServer({ host: '127.0.0.1', port: 0 }); + const { url } = serverInstance; + + const res = await fetch(`${url}/api/health`, { method: 'POST' }); + expect(res.status).toBe(405); + expect(res.headers.get('allow')).toBe('GET'); + }); + + it('returns 404 Not Found for unknown routes', async () => { + serverInstance = await createHttpServer({ host: '127.0.0.1', port: 0 }); + const { url } = serverInstance; + + const res = await fetch(`${url}/unknown-route`); + expect(res.status).toBe(404); + const body = (await res.json()) as { error: string }; + expect(body).toEqual({ error: 'not_found' }); + }); + + it('serves the built web shell from GET / when present', async () => { + serverInstance = await createHttpServer({ host: '127.0.0.1', port: 0 }); + const { url } = serverInstance; + + const res = await fetch(`${url}/`); + + expect(res.status).toBe(200); + expect(res.headers.get('content-type')).toContain('text/html'); + await expect(res.text()).resolves.toContain('
'); + }); + + it('serves built JavaScript assets and supports HEAD for static files', async () => { + serverInstance = await createHttpServer({ host: '127.0.0.1', port: 0 }); + const { url } = serverInstance; + + const html = await fetch(`${url}/`).then((response) => response.text()); + const assetPath = html.match(/src="([^"]+assets\/[^"]+\.js)"/)?.[1]; + + expect(assetPath).toBeDefined(); + + const headResponse = await fetch(`${url}/`, { method: 'HEAD' }); + expect(headResponse.status).toBe(200); + expect(await headResponse.text()).toBe(''); + + const assetResponse = await fetch(`${url}${assetPath}`); + expect(assetResponse.status).toBe(200); + expect(assetResponse.headers.get('content-type')).toContain('text/javascript'); + }); +}); diff --git a/tests/integration/mcp-stdio.test.ts b/tests/integration/mcp-stdio.test.ts new file mode 100644 index 0000000..3d2b204 --- /dev/null +++ b/tests/integration/mcp-stdio.test.ts @@ -0,0 +1,46 @@ +import { mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { execSync } from 'node:child_process'; + +describe('mcp-stdio integration', () => { + beforeAll(() => { + execSync('pnpm build:node', { stdio: 'inherit' }); + }); + + it('spawns built MCP stdio process and calls relay_health tool cleanly', async () => { + const builtJsPath = join(process.cwd(), 'dist', 'mcp', 'main.js'); + const launchDir = mkdtempSync(join(tmpdir(), 'relay-mcp-launch-')); + + const transport = new StdioClientTransport({ + command: 'node', + args: [builtJsPath], + cwd: launchDir, + }); + + const client = new Client({ name: 'integration-tester', version: '1.0.0' }); + + await client.connect(transport); + + const tools = await client.listTools(); + expect(tools.tools.some((t) => t.name === 'relay_health')).toBe(true); + + const res = (await client.callTool({ name: 'relay_health', arguments: {} })) as { + content: Array<{ type: string; text: string }>; + }; + expect(res.content[0]?.type).toBe('text'); + if (res.content[0]?.type === 'text') { + const payload = JSON.parse(res.content[0].text) as { + name: string; + status: string; + version: string; + }; + expect(payload).toEqual({ name: 'relay', status: 'ok', version: '0.1.0' }); + } + + await transport.close(); + }); +}); diff --git a/tests/support/temporary-database.ts b/tests/support/temporary-database.ts new file mode 100644 index 0000000..1ab74bc --- /dev/null +++ b/tests/support/temporary-database.ts @@ -0,0 +1,29 @@ +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import type Database from 'better-sqlite3'; +import { createDatabaseConnection } from '../../src/database/connection.js'; + +export interface TemporaryDatabaseContext { + readonly dir: string; + readonly dbPath: string; + readonly db: Database.Database; + readonly cleanup: () => void; +} + +export function createTemporaryDatabase(): TemporaryDatabaseContext { + const dir = mkdtempSync(join(tmpdir(), 'relay-test-')); + const dbPath = join(dir, 'test.db'); + const db = createDatabaseConnection({ path: dbPath }); + + const cleanup = () => { + try { + db.close(); + } catch { + // ignore + } + rmSync(dir, { recursive: true, force: true }); + }; + + return { dir, dbPath, db, cleanup }; +} diff --git a/tests/unit/application/get-health.test.ts b/tests/unit/application/get-health.test.ts new file mode 100644 index 0000000..c8f9a7d --- /dev/null +++ b/tests/unit/application/get-health.test.ts @@ -0,0 +1,13 @@ +import { describe, it, expect } from 'vitest'; +import { getHealth } from '../../../src/application/health/get-health.js'; + +describe('getHealth', () => { + it('returns exact deterministic health status contract', () => { + const health = getHealth(); + expect(health).toEqual({ + name: 'relay', + status: 'ok', + version: '0.1.0', + }); + }); +}); diff --git a/tests/unit/database/connection.test.ts b/tests/unit/database/connection.test.ts new file mode 100644 index 0000000..f8ce233 --- /dev/null +++ b/tests/unit/database/connection.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; + +const mockPragma = vi.fn(); +const mockClose = vi.fn(); +const mockDatabase = vi.fn(function MockDatabase() { + return { + pragma: mockPragma, + close: mockClose, + readonly: false, + }; +}); + +vi.mock('better-sqlite3', () => ({ + default: mockDatabase, +})); + +describe('createDatabaseConnection', () => { + beforeEach(() => { + vi.resetModules(); + mockDatabase.mockClear(); + mockPragma.mockReset(); + mockClose.mockReset(); + }); + + it('closes writable file-backed databases when WAL mode cannot be enabled', async () => { + mockPragma + .mockReturnValueOnce(undefined) + .mockReturnValueOnce('delete') + .mockReturnValueOnce(undefined); + + const { createDatabaseConnection } = await import('../../../src/database/connection.js'); + + expect(() => createDatabaseConnection({ path: 'tmp/test.db' })).toThrow(/journal mode/i); + expect(mockClose).toHaveBeenCalledTimes(1); + }); +}); diff --git a/tests/unit/database/database-config.test.ts b/tests/unit/database/database-config.test.ts new file mode 100644 index 0000000..097af5b --- /dev/null +++ b/tests/unit/database/database-config.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { + resolveDatabasePath, + getDefaultDatabasePath, +} from '../../../src/database/database-config.js'; + +describe('resolveDatabasePath', () => { + const origEnv = process.env.RELAY_DB_PATH; + + afterEach(() => { + if (origEnv !== undefined) { + process.env.RELAY_DB_PATH = origEnv; + } else { + delete process.env.RELAY_DB_PATH; + } + }); + + it('prefers explicit argument over environment variable', () => { + process.env.RELAY_DB_PATH = '/env/path.db'; + const path = resolveDatabasePath('/explicit/path.db'); + expect(path).toBe('/explicit/path.db'); + }); + + it('uses RELAY_DB_PATH env var when no explicit path passed', () => { + process.env.RELAY_DB_PATH = '/env/path.db'; + const path = resolveDatabasePath(); + expect(path).toBe('/env/path.db'); + }); + + it('rejects empty or whitespace-only explicit path', () => { + expect(() => resolveDatabasePath('')).toThrow(); + expect(() => resolveDatabasePath(' ')).toThrow(); + }); + + it('returns default database path when neither explicit path nor env var is provided', () => { + delete process.env.RELAY_DB_PATH; + const path = resolveDatabasePath(); + expect(path).toBe(getDefaultDatabasePath()); + }); +}); diff --git a/tests/unit/database/migration.test.ts b/tests/unit/database/migration.test.ts new file mode 100644 index 0000000..9adffde --- /dev/null +++ b/tests/unit/database/migration.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { computeChecksum, loadMigrationFiles } from '../../../src/database/migration.js'; + +describe('migration unit tests', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'relay-migration-test-')); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + describe('computeChecksum', () => { + it('computes deterministic sha256 checksum for string content', () => { + const hash1 = computeChecksum('SELECT 1;'); + const hash2 = computeChecksum('SELECT 1;'); + const hash3 = computeChecksum('SELECT 2;'); + + expect(hash1).toBe(hash2); + expect(hash1).not.toBe(hash3); + expect(hash1).toHaveLength(64); + }); + }); + + describe('loadMigrationFiles', () => { + it('loads and sorts migration files by version number', () => { + writeFileSync(join(tempDir, '0002_second.sql'), 'SELECT 2;'); + writeFileSync(join(tempDir, '0001_first.sql'), 'SELECT 1;'); + + const files = loadMigrationFiles(tempDir); + + expect(files).toHaveLength(2); + expect(files[0]?.version).toBe(1); + expect(files[0]?.name).toBe('first'); + expect(files[0]?.filename).toBe('0001_first.sql'); + expect(files[0]?.checksum).toBe(computeChecksum('SELECT 1;')); + + expect(files[1]?.version).toBe(2); + expect(files[1]?.name).toBe('second'); + }); + + it('ignores non-sql files', () => { + writeFileSync(join(tempDir, '0001_first.sql'), 'SELECT 1;'); + writeFileSync(join(tempDir, 'README.md'), '# Notes'); + + const files = loadMigrationFiles(tempDir); + expect(files).toHaveLength(1); + }); + + it('throws RelayError for malformed migration filenames', () => { + writeFileSync(join(tempDir, 'invalid.sql'), 'SELECT 1;'); + + expect(() => loadMigrationFiles(tempDir)).toThrow('Malformed migration filename'); + }); + + it('throws RelayError for duplicate migration versions', () => { + writeFileSync(join(tempDir, '0001_first.sql'), 'SELECT 1;'); + writeFileSync(join(tempDir, '0001_another.sql'), 'SELECT 2;'); + + expect(() => loadMigrationFiles(tempDir)).toThrow('Duplicate migration version'); + }); + }); +}); diff --git a/tests/unit/interfaces/http/create-http-server.test.ts b/tests/unit/interfaces/http/create-http-server.test.ts new file mode 100644 index 0000000..25b92ed --- /dev/null +++ b/tests/unit/interfaces/http/create-http-server.test.ts @@ -0,0 +1,84 @@ +import { execSync } from 'node:child_process'; +import { afterEach, beforeAll, describe, expect, it } from 'vitest'; +import { + resolveHttpPort, + createHttpServer, + getContentType, + resolveStaticAsset, +} from '../../../../src/interfaces/http/create-http-server.js'; +import { RelayError } from '../../../../src/shared/errors.js'; + +describe('resolveHttpPort', () => { + const origEnv = process.env.RELAY_HTTP_PORT; + + afterEach(() => { + if (origEnv !== undefined) { + process.env.RELAY_HTTP_PORT = origEnv; + } else { + delete process.env.RELAY_HTTP_PORT; + } + }); + + it('returns explicit port when valid', () => { + expect(resolveHttpPort(8080)).toBe(8080); + }); + + it('throws RelayError for invalid explicit port', () => { + expect(() => resolveHttpPort(-1)).toThrow(RelayError); + expect(() => resolveHttpPort(70000)).toThrow(RelayError); + }); + + it('uses RELAY_HTTP_PORT environment variable when valid', () => { + process.env.RELAY_HTTP_PORT = '9090'; + expect(resolveHttpPort()).toBe(9090); + }); + + it('throws RelayError for invalid RELAY_HTTP_PORT environment variable', () => { + process.env.RELAY_HTTP_PORT = 'invalid'; + expect(() => resolveHttpPort()).toThrow(RelayError); + }); + + it('defaults to 43110 when no explicit port or env var is set', () => { + delete process.env.RELAY_HTTP_PORT; + expect(resolveHttpPort()).toBe(43110); + }); +}); + +beforeAll(() => { + execSync('pnpm build:web', { stdio: 'inherit' }); +}); + +describe('createHttpServer security restrictions', () => { + it('throws RelayError if host is not loopback', async () => { + await expect(createHttpServer({ host: '0.0.0.0' })).rejects.toThrow(RelayError); + }); +}); + +describe('getContentType', () => { + it('returns expected content types for supported file extensions', () => { + expect(getContentType('index.html')).toBe('text/html; charset=utf-8'); + expect(getContentType('bundle.js')).toBe('text/javascript; charset=utf-8'); + expect(getContentType('styles.css')).toBe('text/css; charset=utf-8'); + expect(getContentType('health.json')).toBe('application/json; charset=utf-8'); + expect(getContentType('logo.svg')).toBe('image/svg+xml'); + expect(getContentType('favicon.ico')).toBe('image/x-icon'); + expect(getContentType('archive.bin')).toBe('application/octet-stream'); + }); +}); + +describe('resolveStaticAsset', () => { + it('resolves the built index.html asset from the web output directory', () => { + const assetPath = resolveStaticAsset('/'); + + expect(assetPath).toBeTruthy(); + expect(assetPath).toMatch(/dist[\\/]web[\\/]index\.html$/); + }); + + it('rejects path traversal outside the built web directory', () => { + expect(resolveStaticAsset('/../package.json')).toBeNull(); + }); + + it('returns null for unknown static files', () => { + expect(resolveStaticAsset('/assets/does-not-exist.js')).toBeNull(); + }); +}); diff --git a/tests/unit/interfaces/mcp/create-mcp-server.test.ts b/tests/unit/interfaces/mcp/create-mcp-server.test.ts new file mode 100644 index 0000000..954a546 --- /dev/null +++ b/tests/unit/interfaces/mcp/create-mcp-server.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from 'vitest'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { createMcpServer } from '../../../../src/interfaces/mcp/create-mcp-server.js'; + +describe('createMcpServer', () => { + it('exposes relay_health tool via in-memory transport', async () => { + const server = createMcpServer(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + const client = new Client({ name: 'test-client', version: '1.0.0' }); + + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]); + + const tools = await client.listTools(); + expect(tools.tools.some((t) => t.name === 'relay_health')).toBe(true); + + const result = (await client.callTool({ name: 'relay_health', arguments: {} })) as { + content: Array<{ type: string; text: string }>; + }; + expect(result.content[0]?.type).toBe('text'); + if (result.content[0]?.type === 'text') { + const parsed = JSON.parse(result.content[0].text) as { + name: string; + status: string; + version: string; + }; + expect(parsed).toEqual({ name: 'relay', status: 'ok', version: '0.1.0' }); + } + }); +}); diff --git a/tests/unit/scripts/validate-repository-assets.test.ts b/tests/unit/scripts/validate-repository-assets.test.ts new file mode 100644 index 0000000..9e0f448 --- /dev/null +++ b/tests/unit/scripts/validate-repository-assets.test.ts @@ -0,0 +1,94 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { validateRepositoryAssets } from '../../../scripts/validate-repository-assets.js'; + +function createFixtureRoot(): string { + const rootDir = mkdtempSync(join(tmpdir(), 'relay-asset-validator-')); + + const requiredDirs = [ + 'src/application/health', + 'src/database', + 'src/interfaces/mcp', + 'src/interfaces/http', + 'web/src', + ]; + + for (const dir of requiredDirs) { + mkdirSync(join(rootDir, dir), { recursive: true }); + } + + writeFileSync(join(rootDir, '.nvmrc'), '24\n'); + writeFileSync(join(rootDir, '.editorconfig'), 'root = true\n'); + writeFileSync(join(rootDir, '.gitignore'), 'dist/\n'); + writeFileSync(join(rootDir, '.prettierrc.json'), '{}\n'); + writeFileSync(join(rootDir, 'eslint.config.js'), 'export default [];\n'); + writeFileSync(join(rootDir, 'tsconfig.base.json'), '{}\n'); + writeFileSync( + join(rootDir, 'package.json'), + JSON.stringify({ + name: 'relay', + version: '0.1.0', + bin: { + 'relay-mcp': './dist/mcp/main.js', + }, + }), + ); + writeFileSync( + join(rootDir, 'README.md'), + '# Relay\n\n[Decision](docs/decisions/0001-product-and-architecture.md)\n', + ); + writeFileSync(join(rootDir, 'src/application/health/get-health.ts'), 'export {};\n'); + writeFileSync(join(rootDir, 'src/database/connection.ts'), 'export {};\n'); + writeFileSync(join(rootDir, 'src/interfaces/mcp/create-mcp-server.ts'), 'export {};\n'); + writeFileSync(join(rootDir, 'src/interfaces/http/create-http-server.ts'), 'export {};\n'); + writeFileSync(join(rootDir, 'web/src/App.tsx'), 'export function App() { return null; }\n'); + mkdirSync(join(rootDir, 'docs/decisions'), { recursive: true }); + writeFileSync(join(rootDir, 'docs/decisions/0001-product-and-architecture.md'), '# decision\n'); + mkdirSync(join(rootDir, 'dist/mcp'), { recursive: true }); + writeFileSync(join(rootDir, 'dist/mcp/main.js'), 'console.log("ok");\n'); + + return rootDir; +} + +describe('validateRepositoryAssets', () => { + const createdRoots: string[] = []; + + afterEach(() => { + for (const rootDir of createdRoots) { + rmSync(rootDir, { recursive: true, force: true }); + } + createdRoots.splice(0, createdRoots.length); + }); + + it('rejects broken README local links', () => { + const rootDir = createFixtureRoot(); + createdRoots.push(rootDir); + + writeFileSync(join(rootDir, 'README.md'), '# Relay\n\n[Missing](docs/missing.md)\n'); + + expect(() => validateRepositoryAssets({ rootDir })).toThrow(/README/i); + }); + + it('rejects unresolved placeholder markers in committed source and docs', () => { + const rootDir = createFixtureRoot(); + createdRoots.push(rootDir); + + writeFileSync( + join(rootDir, 'src/interfaces/http/create-http-server.ts'), + `// ${'TO' + 'DO'} fix me\n`, + ); + + expect(() => validateRepositoryAssets({ rootDir })).toThrow(new RegExp('TO' + 'DO', 'i')); + }); + + it('rejects unresolved placeholder markers in committed html assets', () => { + const rootDir = createFixtureRoot(); + createdRoots.push(rootDir); + + writeFileSync(join(rootDir, 'web', 'index.html'), `\n`); + + expect(() => validateRepositoryAssets({ rootDir })).toThrow(new RegExp('TO' + 'DO', 'i')); + }); +}); diff --git a/tests/unit/shared/package-metadata.test.ts b/tests/unit/shared/package-metadata.test.ts new file mode 100644 index 0000000..3c90386 --- /dev/null +++ b/tests/unit/shared/package-metadata.test.ts @@ -0,0 +1,35 @@ +import { mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { getPackageMetadata } from '../../../src/shared/package-metadata.js'; + +describe('package-metadata', () => { + const originalCwd = process.cwd(); + + afterEach(() => { + process.chdir(originalCwd); + }); + + it('returns package name and version', () => { + const meta = getPackageMetadata(); + expect(meta.name).toBe('relay'); + expect(meta.version).toMatch(/^\d+\.\d+\.\d+/); + }); + + it('loads package metadata when invoked outside the repository working directory', () => { + const tempDir = mkdtempSync(join(tmpdir(), 'relay-package-meta-')); + process.chdir(tempDir); + + vi.resetModules(); + + return import('../../../src/shared/package-metadata.js').then(({ getPackageMetadata }) => { + const meta = getPackageMetadata(); + + expect(meta).toEqual({ + name: 'relay', + version: '0.1.0', + }); + }); + }); +}); diff --git a/tests/unit/web/health-client.test.ts b/tests/unit/web/health-client.test.ts new file mode 100644 index 0000000..6b89d8b --- /dev/null +++ b/tests/unit/web/health-client.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { fetchHealth } from '../../../web/src/api/health-client.js'; + +describe('health-client', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('parses valid /api/health response', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ name: 'relay', status: 'ok', version: '0.1.0' }), + }), + ); + + const health = await fetchHealth(); + expect(health).toEqual({ name: 'relay', status: 'ok', version: '0.1.0' }); + }); + + it('throws on non-2xx response', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: false, + status: 500, + }), + ); + + await expect(fetchHealth()).rejects.toThrow('Health check failed with status 500'); + }); + + it('throws on malformed 200 response payload', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ status: 'ok', version: 1 }), + }), + ); + + await expect(fetchHealth()).rejects.toThrow('Invalid health check response schema'); + }); +}); diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..b4b3875 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "useUnknownInCatchVariables": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b0aaffb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.web.json" }, + { "path": "./tsconfig.test.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..53bb99b --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["src/**/*.test.ts", "web/**/*"] +} diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..9bacb7c --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,21 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "composite": true, + "moduleResolution": "Bundler", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "types": ["node", "vitest/globals"], + "noEmit": true, + "allowJs": true + }, + "include": [ + "src/**/*", + "web/src/**/*", + "tests/**/*", + "scripts/**/*", + "*.config.ts", + "*.config.js" + ] +} diff --git a/tsconfig.web.json b/tsconfig.web.json new file mode 100644 index 0000000..c11fb5f --- /dev/null +++ b/tsconfig.web.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "noEmit": true + }, + "include": ["web/src/**/*"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..799e767 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: { + 'mcp/main': 'src/interfaces/mcp/main.ts', + 'http/main': 'src/interfaces/http/main.ts', + }, + format: ['esm'], + target: 'node24', + outDir: 'dist', + clean: false, + sourcemap: true, + bundle: true, + shims: true, +}); diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..ca1a522 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + root: 'web', + build: { + outDir: '../dist/web', + emptyOutDir: true, + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://127.0.0.1:43110', + changeOrigin: true, + }, + }, + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..5ac3fab --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + include: ['tests/**/*.test.ts', 'web/src/**/*.test.tsx'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + include: [ + 'src/application/**/*.ts', + 'src/database/*.ts', + 'src/interfaces/mcp/create-mcp-server.ts', + 'src/interfaces/http/create-http-server.ts', + 'web/src/api/**/*.ts', + 'web/src/App.tsx', + ], + exclude: ['src/application/health/health.ts', 'src/**/*.d.ts'], + thresholds: { + statements: 80, + branches: 80, + functions: 80, + lines: 80, + }, + }, + }, +}); diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..f717541 --- /dev/null +++ b/web/index.html @@ -0,0 +1,12 @@ + + + + + + Relay + + +
+ + + diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx new file mode 100644 index 0000000..3801ace --- /dev/null +++ b/web/src/App.test.tsx @@ -0,0 +1,76 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { App } from './App.js'; +import * as healthClient from './api/health-client.js'; + +describe('App component', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('renders loading, then success state', async () => { + vi.spyOn(healthClient, 'fetchHealth').mockResolvedValue({ + name: 'relay', + status: 'ok', + version: '0.1.0', + }); + + render(); + + expect(screen.getByTestId('status-loading')).toBeDefined(); + + await waitFor(() => { + expect(screen.getByTestId('status-success')).toBeDefined(); + }); + expect(screen.getByText('Connected (v0.1.0)')).toBeDefined(); + }); + + it('renders error state and handles retry button click', async () => { + const fetchSpy = vi + .spyOn(healthClient, 'fetchHealth') + .mockRejectedValueOnce(new Error('Connection refused')) + .mockResolvedValueOnce({ name: 'relay', status: 'ok', version: '0.1.0' }); + + render(); + + await waitFor(() => { + expect(screen.getByTestId('status-error')).toBeDefined(); + }); + + const retryBtn = screen.getByText('Retry'); + fireEvent.click(retryBtn); + + await waitFor(() => { + expect(screen.getByTestId('status-success')).toBeDefined(); + }); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); + + it('aborts the active retry request when the component unmounts', async () => { + const abortError = Object.assign(new Error('Aborted'), { name: 'AbortError' }); + vi.spyOn(healthClient, 'fetchHealth') + .mockRejectedValueOnce(new Error('Connection refused')) + .mockImplementationOnce( + (signal?: AbortSignal) => + new Promise((_, reject) => { + signal?.addEventListener('abort', () => reject(abortError), { once: true }); + }), + ); + + const { unmount } = render(); + + await waitFor(() => { + expect(screen.getByTestId('status-error')).toBeDefined(); + }); + + fireEvent.click(screen.getByText('Retry')); + unmount(); + + await waitFor(() => { + expect(healthClient.fetchHealth).toHaveBeenCalledTimes(2); + }); + + const retrySignal = vi.mocked(healthClient.fetchHealth).mock.calls[1]?.[0]; + expect(retrySignal?.aborted).toBe(true); + }); +}); diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..f63840a --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,72 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { fetchHealth, type HealthStatusResponse } from './api/health-client.js'; + +export function App() { + const [health, setHealth] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const activeControllerRef = useRef(null); + + const loadHealth = useCallback(() => { + activeControllerRef.current?.abort(); + setLoading(true); + setError(null); + const controller = new AbortController(); + activeControllerRef.current = controller; + + fetchHealth(controller.signal) + .then((data) => { + if (controller.signal.aborted || activeControllerRef.current !== controller) { + return; + } + setHealth(data); + setLoading(false); + activeControllerRef.current = null; + }) + .catch((err: unknown) => { + if (controller.signal.aborted || activeControllerRef.current !== controller) { + return; + } + if (err instanceof Error && err.name === 'AbortError') return; + setError(err instanceof Error ? err.message : 'Unknown error'); + setLoading(false); + activeControllerRef.current = null; + }); + }, []); + + useEffect(() => { + loadHealth(); + + return () => { + activeControllerRef.current?.abort(); + activeControllerRef.current = null; + }; + }, [loadHealth]); + + return ( +
+

Relay

+

Local task sidecar for human–AI workflows.

+ +
+ {loading &&

Checking local service…

} + {!loading && error && ( +
+

Relay service unavailable

+ +
+ )} + {!loading && health &&

Connected (v{health.version})

} +
+
+ ); +} diff --git a/web/src/api/health-client.ts b/web/src/api/health-client.ts new file mode 100644 index 0000000..1b52550 --- /dev/null +++ b/web/src/api/health-client.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +export const HealthStatusSchema = z.object({ + name: z.literal('relay'), + status: z.literal('ok'), + version: z.string(), +}); + +export type HealthStatusResponse = z.infer; + +export async function fetchHealth(signal?: AbortSignal): Promise { + const init: RequestInit = signal ? { signal } : {}; + const res = await fetch('/api/health', init); + if (!res.ok) { + throw new Error(`Health check failed with status ${res.status}`); + } + const data = (await res.json()) as unknown; + const parsed = HealthStatusSchema.safeParse(data); + if (!parsed.success) { + throw new Error('Invalid health check response schema'); + } + return parsed.data; +} diff --git a/web/src/env.d.ts b/web/src/env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/web/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..063baac --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,12 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App.js'; + +const rootEl = document.getElementById('root'); +if (rootEl) { + createRoot(rootEl).render( + + + , + ); +} diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/web/src/vite-env.d.ts @@ -0,0 +1 @@ +///