From 5e2abc876f61afa77fc6a5dfaa30ddf2004e1d60 Mon Sep 17 00:00:00 2001 From: nembal Date: Sun, 2 Aug 2026 02:29:23 +0700 Subject: [PATCH 1/4] feat: add Agent Community read-only CLI --- .github/workflows/ci.yml | 53 + AGENTS.md | 43 + README.md | 71 +- SECURITY.md | 7 + contracts/page.lock.json | 6 + contracts/page/1.0.0/auth.json | 405 ++ contracts/page/1.0.0/batch.json | 800 ++++ contracts/page/1.0.0/manifest.json | 40 + contracts/page/1.0.0/manifest.sha256 | 1 + contracts/page/1.0.0/mcp.json | 1286 ++++++ contracts/page/1.0.0/openapi.json | 4960 +++++++++++++++++++++ contracts/page/1.0.0/rest.json | 116 + eslint.config.js | 15 + package-lock.json | 4629 +++++++++++++++++++ package.json | 57 + scripts/audit-package.ts | 66 + scripts/check-contract-compat.ts | 45 + scripts/sync-page-contracts.ts | 141 + src/__tests__/commands.test.ts | 166 + src/__tests__/contracts.test.ts | 29 + src/__tests__/http.test.ts | 47 + src/__tests__/mcp.test.ts | 53 + src/__tests__/package-boundary.test.ts | 32 + src/__tests__/sync-page-contracts.test.ts | 61 + src/cli.ts | 220 + src/commands/batch.ts | 30 + src/commands/content.ts | 39 + src/commands/docs.ts | 20 + src/commands/member.ts | 12 + src/commands/stats.ts | 7 + src/commands/verify.ts | 17 + src/config.ts | 18 + src/contracts.ts | 137 + src/errors.ts | 19 + src/http.ts | 129 + src/mcp.ts | 96 + tsconfig.json | 15 + tsup.config.ts | 13 + vitest.config.ts | 8 + 39 files changed, 13907 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 AGENTS.md create mode 100644 SECURITY.md create mode 100644 contracts/page.lock.json create mode 100644 contracts/page/1.0.0/auth.json create mode 100644 contracts/page/1.0.0/batch.json create mode 100644 contracts/page/1.0.0/manifest.json create mode 100644 contracts/page/1.0.0/manifest.sha256 create mode 100644 contracts/page/1.0.0/mcp.json create mode 100644 contracts/page/1.0.0/openapi.json create mode 100644 contracts/page/1.0.0/rest.json create mode 100644 eslint.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/audit-package.ts create mode 100644 scripts/check-contract-compat.ts create mode 100644 scripts/sync-page-contracts.ts create mode 100644 src/__tests__/commands.test.ts create mode 100644 src/__tests__/contracts.test.ts create mode 100644 src/__tests__/http.test.ts create mode 100644 src/__tests__/mcp.test.ts create mode 100644 src/__tests__/package-boundary.test.ts create mode 100644 src/__tests__/sync-page-contracts.test.ts create mode 100644 src/cli.ts create mode 100644 src/commands/batch.ts create mode 100644 src/commands/content.ts create mode 100644 src/commands/docs.ts create mode 100644 src/commands/member.ts create mode 100644 src/commands/stats.ts create mode 100644 src/commands/verify.ts create mode 100644 src/config.ts create mode 100644 src/contracts.ts create mode 100644 src/errors.ts create mode 100644 src/http.ts create mode 100644 src/mcp.ts create mode 100644 tsconfig.json create mode 100644 tsup.config.ts create mode 100644 vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b9214b9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + test: + name: Node ${{ matrix.node }} on ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-14] + node: [22.14.0, "24", "26"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: npm + - run: npm ci + - run: npm run lint + - run: npm test + - run: npm run typecheck + - run: npm run build + + contracts: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npm run contracts:check + + package-audit: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npm run build + - run: npm run package:audit diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..79cd793 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,43 @@ +# Repository guide for coding agents + +## Product boundary + +This repository owns one public TypeScript/ESM package, `@agentcommunity/cli`, and one binary, `agentcommunity`. V1 is read-only, CLI-only, and supports macOS/Linux on Node `^22.14.0 || ^24.0.0 || ^26.0.0`. Do not add a JavaScript SDK export, Windows support claim, workspace coupling to PAGE, auth commands, registration, payment behavior, telemetry, an update ping, or a runtime `--base-url` without a separately approved task. + +The seven commands are `stats`, `member`, `verify`, `content list`, `content search`, `docs ask`, and `batch`. `register_agent` is catalog evidence only: no runtime path may call it. Link the specialist `@agentcommunity/dmv-agent` and `@agentcommunity/aid-doctor` instead of wrapping or copying them. + +## Architecture + +- `src/cli.ts` owns argument parsing, stdout/stderr, and stable exit-code mapping. Command modules never call `process.exit`. +- `src/http.ts` is the only production network boundary. Keep the origin fixed, redirects manual, timeouts and byte caps explicit, MIME/JSON/schema validation strict, and errors sanitized. Do not add automatic ordinary-command retries. +- `src/mcp.ts` is a narrow modern `2026-07-28` client. Runtime commands call one tool directly and never add a `tools/list` round trip. +- `src/commands/` modules orchestrate injected HTTP/MCP/filesystem/output boundaries and return typed results. +- `src/contracts.ts` validates runtime payloads and the vendored PAGE bundle. Contract scripts may fetch only for explicit maintenance; install and normal execution remain offline except for the requested command. + +Stable exits are: `0` success, `2` usage/local input, `3` domain-negative, `4` reserved for auth, `5` protocol/schema/contract, `6` timeout/unavailable, `7` rate limit, and `8` mixed batch. + +## Contract policy + +`contracts/page/1.0.0/` must be byte-identical to the approved immutable PAGE bundle. `contracts/page.lock.json` pins version `1.0.0`, compatible range `^1.0.0`, the exact HTTPS manifest URL, and its `sha256:` hash. Never hand-edit a vendored payload or replace an immutable version. Use `npm run contracts:sync` only after PAGE publishes an approved version and the lock is intentionally reviewed. Sync must validate all bytes before writing; `npm run contracts:check` fails closed on hash, inventory, revision, fixture, or exact tool-order drift. + +## Development and tests + +Use test-driven development: add a focused failing fixture test, confirm the expected RED state, implement the minimum behavior, then rerun focused and full tests. Tests inject transports and must not depend on production network access. + +```sh +npm ci +npm run lint +npm test +npm run typecheck +npm run build +npm run contracts:check +npm run package:audit +``` + +The final package audit must inspect the exact tarball allowlist and metadata, scan packed files for likely secrets, install that tarball in a clean temporary project, and run `npx --no-install agentcommunity --help`. CI covers Node 22.14, 24, and 26 on Ubuntu 24.04 and macOS. + +## Security and release gates + +Never log request bodies, credentials, or token-like values. Do not add production credentials to tests or CI. Keep package install scripts absent. Production URLs remain exact HTTPS Agent Community URLs; reject redirects and path escapes. Batch input contains no item URL, headers, credentials, or member operations. + +There is intentionally no `release.yml`. Do not publish, push, deploy, create credentials, or add OIDC permissions without explicit owner authorization. Keep these states distinct in docs and reports: source complete, npm package published, PAGE endpoint deployed/production-capable, PAGE linked/discoverable. The current batch source awaits PAGE production deployment. diff --git a/README.md b/README.md index 7e9cbe3..275b4de 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,69 @@ -# cli -Official command-line client for Agent Community agent interfaces +# Agent Community CLI + +`@agentcommunity/cli` is the standalone, read-only command-line client for Agent Community's public agent interfaces. It provides seven commands and does not expose a public JavaScript SDK. + +The source is complete for macOS and Linux on Node.js `^22.14.0 || ^24.0.0 || ^26.0.0`. The package is not yet published, PAGE has not yet linked it, and the branch-local batch endpoint must be deployed before `batch` is production-capable. Do not treat source completion as npm publication, production availability, or Agent Community discovery linkage. + +## Source checkout usage + +An npm install command will be added only after the package is actually published. From a source checkout: + +```sh +npm ci +npm run build +node dist/cli.js --help +``` + +Windows is not supported in v1. + +## Commands + +```text +agentcommunity stats +agentcommunity member +agentcommunity verify +agentcommunity content list [--type docs|blog|page] [--limit 1..50] [--cursor opaque] +agentcommunity content search [--type docs|blog|page] [--limit 1..50] [--cursor opaque] +agentcommunity docs ask [--top-k 1..10] +agentcommunity batch +``` + +Every command accepts `--json` and `--timeout `. The per-call timeout defaults to 10,000 ms and must be between 1,000 and 30,000 ms. `--json` writes exactly one JSON value followed by LF. Human-readable output is the default and honors `NO_COLOR` (the CLI currently emits no ANSI color). Local, network, and protocol errors write one stable JSON error envelope to stderr and nothing to stdout. Semantic-negative service results still print their payload and return a nonzero status. + +`stats` calls only modern MCP `get_community_stats`. `member` is an exact name-or-slug lookup through `lookup_member`; it never enumerates the directory or falls back to content or map search. `verify` calls only `verify_certificate`. `content list` and `content search` use `/api/v1/content`; an empty page is successful. `docs ask` posts a non-streaming request directly to `/ask`. `batch` accepts a strict JSON file or `-` for stdin, caps input at 262,144 bytes before parsing, permits only `content.list` and `docs.ask`, and preserves item order. + +For write-capable certificate registration use [@agentcommunity/dmv-agent](https://www.npmjs.com/package/@agentcommunity/dmv-agent). For AID diagnostics use [@agentcommunity/aid-doctor](https://www.npmjs.com/package/@agentcommunity/aid-doctor). Their behavior is intentionally not copied into this umbrella CLI. + +## Exit codes + +| Code | Meaning | +|---:|---| +| 0 | Success, including an empty content page, or all batch items succeeded | +| 2 | Usage/local input error or invalid certificate format | +| 3 | Member not found/ambiguous or certificate not issued | +| 4 | Reserved for Task 6.2 auth and credential safety | +| 5 | Remote protocol, schema, or pinned-contract mismatch | +| 6 | Timeout, network failure, upstream unavailable, or certificate verifier unavailable | +| 7 | Rate limited; a valid bounded `Retry-After` value is included when available | +| 8 | Batch transport succeeded but at least one ordered item failed | + +## Privacy and network behavior + +There is no telemetry, analytics identifier, update ping, request-body logging, credential logging, or hidden network request. Production requests are fixed to `https://agentcommunity.org`; there is no runtime `--base-url`. Redirects are rejected, response sizes are capped, JSON MIME and schemas are validated, and ordinary commands are never retried automatically. + +Runtime commands use the committed PAGE contract bundle `1.0.0`, whose manifest SHA-256 is `b1f10b6288e436ccdca282b88a9a9115fcc0f6716f90731aab1455175b535595`. Contract sync is an explicit maintainer operation and never runs during install or normal execution. + +## Contributing + +Read [AGENTS.md](./AGENTS.md) before changing source. The deterministic quality gate is: + +```sh +npm run lint +npm test +npm run typecheck +npm run build +npm run contracts:check +npm run package:audit +``` + +Publishing, release automation, production deployment, and PAGE linking require separate owner authorization and live verification. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..870cd67 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security policy + +Report suspected vulnerabilities privately to `security@agentcommunity.org`. Do not open a public issue containing credentials, tokens, personal data, or an exploitable proof of concept. + +The supported source targets are the maintained Node.js versions declared in `package.json` on macOS and Linux. No npm release has been published yet, so there is no released version support table. + +The CLI sends only explicitly requested read-only operations to fixed `https://agentcommunity.org` endpoints. It has no telemetry, update checks, credential collection, or install-time network script. Redirects are rejected and network errors are sanitized. diff --git a/contracts/page.lock.json b/contracts/page.lock.json new file mode 100644 index 0000000..0965ed2 --- /dev/null +++ b/contracts/page.lock.json @@ -0,0 +1,6 @@ +{ + "bundle_version": "1.0.0", + "compatible_range": "^1.0.0", + "manifest_url": "https://agentcommunity.org/.well-known/agentcommunity-contracts/1.0.0/manifest.json", + "manifest_sha256": "sha256:b1f10b6288e436ccdca282b88a9a9115fcc0f6716f90731aab1455175b535595" +} diff --git a/contracts/page/1.0.0/auth.json b/contracts/page/1.0.0/auth.json new file mode 100644 index 0000000..ad41860 --- /dev/null +++ b/contracts/page/1.0.0/auth.json @@ -0,0 +1,405 @@ +{ + "authorization_server_metadata_url": "https://agentcommunity.org/.well-known/oauth-authorization-server", + "discovery": { + "authorization_server_metadata": { + "agent_auth": { + "claim_endpoint": "https://agentcommunity.org/agent/identity/claim", + "identity_endpoint": "https://agentcommunity.org/agent/identity", + "identity_types_supported": [ + "service_auth" + ], + "skill": "https://agentcommunity.org/auth.md" + }, + "grant_types_supported": [ + "urn:ietf:params:oauth:grant-type:jwt-bearer", + "urn:workos:agent-auth:grant-type:claim" + ], + "issuer": "https://agentcommunity.org", + "jwks_uri": "https://agentcommunity.org/.well-known/jwks.json", + "protected_resources": [ + "https://agentcommunity.org/api" + ], + "revocation_endpoint": "https://agentcommunity.org/oauth2/revoke", + "revocation_endpoint_auth_methods_supported": [ + "none" + ], + "scopes_supported": [ + "agent.account.read", + "agent.registrations.read" + ], + "token_endpoint": "https://agentcommunity.org/oauth2/token", + "token_endpoint_auth_methods_supported": [ + "none" + ] + }, + "protected_resource_metadata": { + "authorization_servers": [ + "https://agentcommunity.org" + ], + "bearer_methods_supported": [ + "header" + ], + "resource": "https://agentcommunity.org/api", + "resource_documentation": "https://agentcommunity.org/auth.md", + "resource_name": "Agent Community agent API", + "resource_policy_uri": "https://agentcommunity.org/terms", + "scopes_supported": [ + "agent.account.read", + "agent.registrations.read" + ] + } + }, + "fixtures": { + "jwt_bearer_refresh": { + "request": { + "form": { + "assertion": "sandbox_identity_assertion_fixture_only", + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "resource": "https://agentcommunity.org/api" + }, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "method": "POST", + "path": "/oauth2/token" + }, + "response": { + "body": { + "access_token": "sandbox_access_token_fixture_only", + "expires_in": 3600, + "scope": "agent.account.read agent.registrations.read", + "token_type": "Bearer" + }, + "http_status": 200 + } + }, + "own_account": { + "request": { + "headers": { + "Accept": "application/json", + "Authorization": "Bearer sandbox_access_token_fixture_only" + }, + "method": "GET", + "path": "/api/v1/agent/account" + }, + "response": { + "body": { + "account": { + "email": "fixture@example.invalid", + "email_verified": true, + "id": "00000000-0000-4000-8000-000000000601" + }, + "authorization": { + "access_token_expires_at": "2099-01-01T01:00:00.000Z", + "delegation_expires_at": "2099-01-02T00:00:00.000Z", + "registration_id": "00000000-0000-4000-8000-000000000501", + "scopes": [ + "agent.account.read", + "agent.registrations.read" + ], + "status": "approved" + } + }, + "http_status": 200 + } + }, + "poll_outcomes": { + "access_denied": { + "body": { + "error": "access_denied", + "error_description": "Authorization was denied" + }, + "http_status": 400 + }, + "authorization_pending": { + "body": { + "error": "authorization_pending", + "error_description": "Authorization is still pending" + }, + "http_status": 400 + }, + "expired_token": { + "body": { + "error": "expired_token", + "error_description": "The claim token or current claim attempt has expired" + }, + "http_status": 400 + }, + "slow_down": { + "body": { + "error": "slow_down", + "error_description": "Polling too quickly; increase the interval by 5 seconds" + }, + "http_status": 400 + }, + "success": { + "body": { + "access_token": "sandbox_access_token_fixture_only", + "assertion_expires": "2099-01-01T00:00:00.000Z", + "expires_in": 3600, + "identity_assertion": "sandbox_identity_assertion_fixture_only", + "scope": "agent.account.read agent.registrations.read", + "token_type": "Bearer" + }, + "http_status": 200 + } + }, + "revoke": { + "request": { + "form": { + "token": "sandbox_access_token_fixture_only", + "token_type_hint": "access_token" + }, + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "method": "POST", + "path": "/oauth2/revoke" + }, + "response": { + "body": null, + "http_status": 200 + } + }, + "service_auth": { + "request": { + "body": { + "client_name": "@agentcommunity/cli fixture", + "login_hint": "fixture@example.invalid", + "scopes": [ + "agent.account.read", + "agent.registrations.read" + ], + "type": "service_auth" + }, + "headers": { + "Accept": "application/json", + "Content-Type": "application/json" + }, + "method": "POST", + "path": "/agent/identity" + }, + "response": { + "body": { + "claim": { + "expires_in": 600, + "interval": 5, + "user_code": "000000", + "verification_uri": "https://agentcommunity.org/agent/authorize?claim_attempt_token=sandbox_claim_attempt_fixture_only" + }, + "claim_token": "sandbox_claim_token_fixture_only", + "claim_token_expires": "2099-01-01T00:00:00.000Z", + "claim_url": "https://agentcommunity.org/agent/identity/claim", + "post_claim_scopes": [ + "agent.account.read", + "agent.registrations.read" + ], + "registration_id": "00000000-0000-4000-8000-000000000501", + "registration_type": "service_auth" + }, + "http_status": 200 + } + } + }, + "grant_types": { + "claim": "urn:workos:agent-auth:grant-type:claim", + "jwt_bearer": "urn:ietf:params:oauth:grant-type:jwt-bearer" + }, + "protected_resource_metadata_url": "https://agentcommunity.org/.well-known/oauth-protected-resource/api", + "resource": "https://agentcommunity.org/api", + "schemas": { + "poll_error": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "error": { + "enum": [ + "invalid_request", + "unsupported_grant_type", + "invalid_target", + "invalid_grant", + "authorization_pending", + "slow_down", + "access_denied", + "expired_token", + "invalid_claim_token", + "claim_expired", + "claimed_or_in_flight", + "rate_limited", + "temporarily_unavailable" + ], + "type": "string" + }, + "error_description": { + "type": "string" + } + }, + "required": [ + "error", + "error_description" + ], + "type": "object" + }, + "service_auth_request": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "client_name": { + "type": "string" + }, + "login_hint": { + "format": "email", + "maxLength": 320, + "minLength": 3, + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "type": "string" + }, + "scopes": { + "items": { + "enum": [ + "agent.account.read", + "agent.registrations.read" + ], + "type": "string" + }, + "maxItems": 2, + "minItems": 1, + "type": "array" + }, + "type": { + "const": "service_auth", + "type": "string" + } + }, + "required": [ + "type", + "login_hint" + ], + "type": "object" + }, + "service_auth_response": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "claim": { + "additionalProperties": false, + "properties": { + "expires_in": { + "const": 600, + "type": "number" + }, + "interval": { + "const": 5, + "type": "number" + }, + "user_code": { + "pattern": "^\\d{6}$", + "type": "string" + }, + "verification_uri": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "user_code", + "expires_in", + "verification_uri", + "interval" + ], + "type": "object" + }, + "claim_token": { + "maxLength": 8192, + "minLength": 1, + "pattern": "^[\\u0021-\\u007e]+$", + "type": "string" + }, + "claim_token_expires": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "claim_url": { + "const": "https://agentcommunity.org/agent/identity/claim", + "type": "string" + }, + "post_claim_scopes": { + "items": { + "enum": [ + "agent.account.read", + "agent.registrations.read" + ], + "type": "string" + }, + "maxItems": 2, + "minItems": 1, + "type": "array" + }, + "registration_id": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$", + "type": "string" + }, + "registration_type": { + "const": "service_auth", + "type": "string" + } + }, + "required": [ + "registration_id", + "registration_type", + "claim_url", + "claim_token", + "claim_token_expires", + "post_claim_scopes", + "claim" + ], + "type": "object" + }, + "token_success": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "access_token": { + "maxLength": 8192, + "minLength": 1, + "pattern": "^[\\u0021-\\u007e]+$", + "type": "string" + }, + "assertion_expires": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "expires_in": { + "exclusiveMinimum": 0, + "maximum": 3600, + "type": "integer" + }, + "identity_assertion": { + "maxLength": 8192, + "minLength": 1, + "type": "string" + }, + "scope": { + "minLength": 1, + "type": "string" + }, + "token_type": { + "const": "Bearer", + "type": "string" + } + }, + "required": [ + "access_token", + "token_type", + "expires_in", + "scope", + "identity_assertion", + "assertion_expires" + ], + "type": "object" + } + } +} diff --git a/contracts/page/1.0.0/batch.json b/contracts/page/1.0.0/batch.json new file mode 100644 index 0000000..84e87fa --- /dev/null +++ b/contracts/page/1.0.0/batch.json @@ -0,0 +1,800 @@ +{ + "constants": { + "content_list_max_response_bytes": 262144, + "docs_ask_max_response_bytes": 65536, + "execution": "sequential_concurrency_1", + "max_duration_ms": 10000, + "max_id_chars": 64, + "max_items": 10, + "max_operation_chars": 128, + "max_request_bytes": 262144, + "max_total_cost": 10, + "max_total_response_bytes": 1048576, + "operations": [ + "content.list", + "docs.ask" + ] + }, + "fixtures": { + "deadline_error": { + "response": { + "body": { + "items": [ + { + "error": { + "code": "deadline_exceeded", + "message": "Batch deadline exceeded" + }, + "id": "fixture-deadline", + "operation": "docs.ask", + "status": "error" + } + ] + }, + "http_status": 200 + } + }, + "envelope_errors": [ + { + "body": { + "error": { + "code": "invalid_json", + "message": "Invalid JSON request body" + } + }, + "http_status": 400, + "name": "invalid_json" + }, + { + "body": { + "error": { + "code": "invalid_request", + "message": "Invalid batch request" + } + }, + "http_status": 400, + "name": "invalid_request" + }, + { + "body": { + "error": { + "code": "request_too_large", + "message": "Batch request exceeds 262144 bytes" + } + }, + "http_status": 413, + "name": "request_too_large" + }, + { + "body": { + "error": { + "code": "unsupported_media_type", + "message": "Content-Type must be application/json" + } + }, + "http_status": 415, + "name": "unsupported_media_type" + }, + { + "body": { + "error": { + "code": "rate_limited", + "message": "Too many batch requests" + } + }, + "http_status": 429, + "name": "rate_limited" + }, + { + "body": { + "error": { + "code": "internal_error", + "message": "Batch execution failed" + } + }, + "http_status": 500, + "name": "internal_error" + } + ], + "mixed_ordered_result": { + "request": { + "body": { + "items": [ + { + "arguments": { + "limit": 5, + "query": "agent discovery", + "type": "docs" + }, + "id": "content-1", + "operation": "content.list" + }, + { + "arguments": { + "query": "How can agents discover AgentCommunity resources?", + "top_k": 3 + }, + "id": "docs-1", + "operation": "docs.ask" + } + ] + }, + "headers": { + "Accept": "application/json", + "Content-Type": "application/json" + }, + "method": "POST", + "path": "/api/v1/batch" + }, + "response": { + "body": { + "items": [ + { + "id": "sandbox-content", + "operation": "content.list", + "result": { + "items": [ + { + "description": "Synthetic fixture; no production content or member data.", + "href": "/sandbox/fixtures/content", + "title": "[sandbox fixture] Published content", + "type": "page" + } + ], + "page": { + "has_more": false, + "limit": 1, + "next_cursor": null + } + }, + "status": "ok" + }, + { + "error": { + "code": "unknown_operation", + "message": "Unsupported operation" + }, + "id": "sandbox-unknown", + "operation": "sandbox.unknown", + "status": "error" + } + ] + }, + "http_status": 200 + } + }, + "rate_limit": { + "body": { + "error": { + "code": "rate_limited", + "message": "Too many batch requests" + } + }, + "headers": { + "RateLimit-Policy": "\"agent-batch\";q=10;w=60", + "Retry-After": "60" + }, + "http_status": 429 + } + }, + "schemas": { + "known_item": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "arguments": { + "additionalProperties": false, + "properties": { + "cursor": { + "maxLength": 256, + "type": "string" + }, + "limit": { + "default": 20, + "maximum": 50, + "minimum": 1, + "type": "integer" + }, + "query": { + "maxLength": 200, + "type": "string" + }, + "type": { + "enum": [ + "docs", + "blog", + "page" + ], + "type": "string" + } + }, + "type": "object" + }, + "id": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "type": "string" + }, + "operation": { + "const": "content.list", + "type": "string" + } + }, + "required": [ + "id", + "operation", + "arguments" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "arguments": { + "additionalProperties": false, + "properties": { + "query": { + "maxLength": 500, + "minLength": 2, + "type": "string" + }, + "top_k": { + "default": 5, + "maximum": 10, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "id": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "type": "string" + }, + "operation": { + "const": "docs.ask", + "type": "string" + } + }, + "required": [ + "id", + "operation", + "arguments" + ], + "type": "object" + } + ] + }, + "request": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "items": { + "items": { + "additionalProperties": false, + "properties": { + "arguments": {}, + "id": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "type": "string" + }, + "operation": { + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "operation", + "arguments" + ], + "type": "object" + }, + "maxItems": 10, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "items" + ], + "type": "object" + }, + "response": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "items": { + "items": { + "anyOf": [ + { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "id": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "type": "string" + }, + "operation": { + "const": "content.list", + "type": "string" + }, + "result": { + "additionalProperties": false, + "properties": { + "items": { + "items": { + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "href": { + "pattern": "^\\/(?!\\/)", + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "docs", + "blog", + "page" + ], + "type": "string" + } + }, + "required": [ + "title", + "description", + "href", + "type" + ], + "type": "object" + }, + "maxItems": 50, + "type": "array" + }, + "page": { + "additionalProperties": false, + "properties": { + "has_more": { + "type": "boolean" + }, + "limit": { + "maximum": 50, + "minimum": 1, + "type": "integer" + }, + "next_cursor": { + "anyOf": [ + { + "maxLength": 256, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "limit", + "next_cursor", + "has_more" + ], + "type": "object" + } + }, + "required": [ + "items", + "page" + ], + "type": "object" + }, + "status": { + "const": "ok", + "type": "string" + } + }, + "required": [ + "id", + "operation", + "status", + "result" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "id": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "type": "string" + }, + "operation": { + "const": "docs.ask", + "type": "string" + }, + "result": { + "additionalProperties": false, + "properties": { + "answer": { + "maxLength": 1500, + "type": "string" + }, + "query": { + "maxLength": 500, + "type": "string" + }, + "sources": { + "items": { + "additionalProperties": false, + "properties": { + "description": { + "maxLength": 500, + "type": "string" + }, + "excerpt": { + "maxLength": 320, + "type": "string" + }, + "path": { + "pattern": "^\\/(?!\\/)", + "type": "string" + }, + "title": { + "maxLength": 200, + "type": "string" + }, + "url": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "title", + "description", + "path", + "url", + "excerpt" + ], + "type": "object" + }, + "maxItems": 10, + "type": "array" + } + }, + "required": [ + "query", + "answer", + "sources" + ], + "type": "object" + }, + "status": { + "const": "ok", + "type": "string" + } + }, + "required": [ + "id", + "operation", + "status", + "result" + ], + "type": "object" + } + ] + }, + { + "additionalProperties": false, + "properties": { + "error": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "code": { + "const": "unknown_operation", + "type": "string" + }, + "message": { + "const": "Unsupported operation", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "invalid_arguments", + "type": "string" + }, + "message": { + "const": "Invalid arguments", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "operation_failed", + "type": "string" + }, + "message": { + "const": "Operation failed", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "deadline_exceeded", + "type": "string" + }, + "message": { + "const": "Batch deadline exceeded", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "response_too_large", + "type": "string" + }, + "message": { + "const": "Operation response exceeds its limit", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "total_response_too_large", + "type": "string" + }, + "message": { + "const": "Batch response budget exceeded", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + } + ] + }, + "id": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "type": "string" + }, + "operation": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "status": { + "const": "error", + "type": "string" + } + }, + "required": [ + "id", + "operation", + "status", + "error" + ], + "type": "object" + } + ] + }, + "maxItems": 10, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "items" + ], + "type": "object" + }, + "route_error": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "error": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "code": { + "const": "invalid_json", + "type": "string" + }, + "message": { + "const": "Invalid JSON request body", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "invalid_request", + "type": "string" + }, + "message": { + "const": "Invalid batch request", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "request_too_large", + "type": "string" + }, + "message": { + "const": "Batch request exceeds 262144 bytes", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "unsupported_media_type", + "type": "string" + }, + "message": { + "const": "Content-Type must be application/json", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "rate_limited", + "type": "string" + }, + "message": { + "const": "Too many batch requests", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "internal_error", + "type": "string" + }, + "message": { + "const": "Batch execution failed", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + } + ] + } + }, + "required": [ + "error" + ], + "type": "object" + } + }, + "valid_items": [ + { + "arguments": { + "limit": 5, + "query": "agent discovery", + "type": "docs" + }, + "id": "content-1", + "operation": "content.list" + }, + { + "arguments": { + "query": "How can agents discover AgentCommunity resources?", + "top_k": 3 + }, + "id": "docs-1", + "operation": "docs.ask" + } + ] +} diff --git a/contracts/page/1.0.0/manifest.json b/contracts/page/1.0.0/manifest.json new file mode 100644 index 0000000..746f525 --- /dev/null +++ b/contracts/page/1.0.0/manifest.json @@ -0,0 +1,40 @@ +{ + "bundle_version": "1.0.0", + "contract_mode": "public", + "files": [ + { + "bytes": 12314, + "media_type": "application/json", + "path": "auth.json", + "sha256": "sha256:3807637fa3579d403da7114ab454b08d9285a8fcc1286756ab1e007ee5c0e19d" + }, + { + "bytes": 24500, + "media_type": "application/json", + "path": "batch.json", + "sha256": "sha256:c3922a2348c87cd35148f7ff67ac45fe2dcc857bb7e76c1ec16d9085ba458832" + }, + { + "bytes": 42058, + "media_type": "application/json", + "path": "mcp.json", + "sha256": "sha256:53c6c703f1dcf2bb28a9d31287f001d9644d64900ed39edf027f77bf01640e87" + }, + { + "bytes": 178922, + "media_type": "application/json", + "path": "openapi.json", + "sha256": "sha256:b671e6741e2baf9226fbfc49e3be8726e8e2b80ac0e80dfbb5d45d4d8a09cef2" + }, + { + "bytes": 3409, + "media_type": "application/json", + "path": "rest.json", + "sha256": "sha256:bf4bda59ba26f185282e025b07a0ce0ce76bb8a04e55c6aa715b3fa4058e5309" + } + ], + "format_version": 1, + "openapi_version": "1.3.0", + "page_source": "agentcommunity-page-openapi@1.3.0", + "workos_auth_md_commit": "b53c9edfbfeea679b617727ebca9ba436bade794" +} diff --git a/contracts/page/1.0.0/manifest.sha256 b/contracts/page/1.0.0/manifest.sha256 new file mode 100644 index 0000000..ad84c32 --- /dev/null +++ b/contracts/page/1.0.0/manifest.sha256 @@ -0,0 +1 @@ +b1f10b6288e436ccdca282b88a9a9115fcc0f6716f90731aab1455175b535595 manifest.json diff --git a/contracts/page/1.0.0/mcp.json b/contracts/page/1.0.0/mcp.json new file mode 100644 index 0000000..c3fb8ab --- /dev/null +++ b/contracts/page/1.0.0/mcp.json @@ -0,0 +1,1286 @@ +{ + "advertised_revision": "2026-07-28", + "docs_tools": [ + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true + }, + "description": "Search only published Agent Community documentation. Does not search members, registrations, private lists, or authenticated data.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "limit": { + "default": 5, + "maximum": 10, + "minimum": 1, + "type": "integer" + }, + "query": { + "maxLength": 300, + "minLength": 2, + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_docs", + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "count": { + "maximum": 10, + "minimum": 0, + "type": "integer" + }, + "results": { + "items": { + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "excerpt": { + "maxLength": 320, + "type": "string" + }, + "path": { + "type": "string" + }, + "title": { + "type": "string" + }, + "url": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "title", + "description", + "path", + "url", + "excerpt" + ], + "type": "object" + }, + "maxItems": 10, + "type": "array" + } + }, + "required": [ + "results", + "count" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "invalid_arguments", + "unknown_tool", + "document_not_found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + } + }, + "required": [ + "error" + ], + "type": "object" + } + ], + "type": "object" + }, + "title": "Search Agent Community documentation" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true + }, + "description": "Read one published Agent Community documentation path as Markdown. Allowed paths are /developers, /docs, and /docs/*.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "path": { + "maxLength": 300, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "name": "get_doc", + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "markdown": { + "maxLength": 100000, + "type": "string" + }, + "title": { + "type": "string" + }, + "url": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "title", + "url", + "markdown" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "invalid_arguments", + "unknown_tool", + "document_not_found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + } + }, + "required": [ + "error" + ], + "type": "object" + } + ], + "type": "object" + }, + "title": "Read an Agent Community document" + } + ], + "fixtures": { + "legacy_parameter_exceptions": [ + { + "behavior": "initialize with an empty params object is invalid", + "direction": "rejects_previously_accepted", + "endpoint": "/mcp", + "request": { + "body": { + "id": "fixture-product-legacy", + "jsonrpc": "2.0", + "method": "initialize", + "params": {} + }, + "headers": { + "Accept": "application/json", + "Content-Type": "application/json" + } + }, + "response": { + "body": { + "error": { + "code": -32602, + "message": "Invalid params" + }, + "id": "fixture-product-legacy", + "jsonrpc": "2.0" + }, + "http_status": 200 + } + }, + { + "behavior": "legacy tools/list normalizes an array params value to an empty object", + "direction": "accepts_previously_rejected", + "endpoint": "/mcp/docs", + "request": { + "body": { + "id": "fixture-docs-legacy", + "jsonrpc": "2.0", + "method": "tools/list", + "params": [] + }, + "headers": { + "Accept": "application/json", + "Content-Type": "application/json" + } + }, + "response": { + "body": { + "id": "fixture-docs-legacy", + "jsonrpc": "2.0", + "result": { + "tools": [ + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true + }, + "description": "Search only published Agent Community documentation. Does not search members, registrations, private lists, or authenticated data.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "limit": { + "default": 5, + "maximum": 10, + "minimum": 1, + "type": "integer" + }, + "query": { + "maxLength": 300, + "minLength": 2, + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_docs", + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "count": { + "maximum": 10, + "minimum": 0, + "type": "integer" + }, + "results": { + "items": { + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "excerpt": { + "maxLength": 320, + "type": "string" + }, + "path": { + "type": "string" + }, + "title": { + "type": "string" + }, + "url": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "title", + "description", + "path", + "url", + "excerpt" + ], + "type": "object" + }, + "maxItems": 10, + "type": "array" + } + }, + "required": [ + "results", + "count" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "invalid_arguments", + "unknown_tool", + "document_not_found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + } + }, + "required": [ + "error" + ], + "type": "object" + } + ], + "type": "object" + }, + "title": "Search Agent Community documentation" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true + }, + "description": "Read one published Agent Community documentation path as Markdown. Allowed paths are /developers, /docs, and /docs/*.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "path": { + "maxLength": 300, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "name": "get_doc", + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "markdown": { + "maxLength": 100000, + "type": "string" + }, + "title": { + "type": "string" + }, + "url": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "title", + "url", + "markdown" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "invalid_arguments", + "unknown_tool", + "document_not_found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + } + }, + "required": [ + "error" + ], + "type": "object" + } + ], + "type": "object" + }, + "title": "Read an Agent Community document" + } + ] + } + }, + "http_status": 200 + } + } + ], + "modern": { + "tool_calls": [ + { + "request": { + "body": { + "id": "fixture-member-call", + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "_meta": { + "io.modelcontextprotocol/clientCapabilities": {}, + "io.modelcontextprotocol/clientInfo": { + "name": "@agentcommunity/cli", + "version": "0.0.0-fixture" + }, + "io.modelcontextprotocol/protocolVersion": "2026-07-28" + }, + "arguments": { + "query": "fixture-member" + }, + "name": "lookup_member" + } + }, + "endpoint": "/mcp", + "headers": { + "Accept": "application/json", + "Content-Type": "application/json", + "MCP-Protocol-Version": "2026-07-28", + "Mcp-Method": "tools/call", + "Mcp-Name": "lookup_member" + } + }, + "response": { + "body": { + "id": "fixture-member-call", + "jsonrpc": "2.0", + "result": { + "_meta": { + "io.modelcontextprotocol/serverInfo": { + "icons": [ + { + "mimeType": "image/png", + "sizes": [ + "180x180" + ], + "src": "https://agentcommunity.org/apple-touch-icon.png" + } + ], + "name": "agentcommunity", + "title": "Agent Community", + "version": "1.0.0", + "websiteUrl": "https://agentcommunity.org" + } + }, + "content": [ + { + "text": "{\"status\":\"member\",\"matches\":[{\"display_name\":\"[fixture] Member\",\"member_since\":null,\"profile_url\":\"https://agentcommunity.org/m/fixture-member\"}]}", + "type": "text" + } + ], + "resultType": "complete", + "structuredContent": { + "matches": [ + { + "display_name": "[fixture] Member", + "member_since": null, + "profile_url": "https://agentcommunity.org/m/fixture-member" + } + ], + "status": "member" + } + } + }, + "http_status": 200 + }, + "tool": "lookup_member" + }, + { + "request": { + "body": { + "id": "fixture-stats-call", + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "_meta": { + "io.modelcontextprotocol/clientCapabilities": {}, + "io.modelcontextprotocol/clientInfo": { + "name": "@agentcommunity/cli", + "version": "0.0.0-fixture" + }, + "io.modelcontextprotocol/protocolVersion": "2026-07-28" + }, + "arguments": {}, + "name": "get_community_stats" + } + }, + "endpoint": "/mcp", + "headers": { + "Accept": "application/json", + "Content-Type": "application/json", + "MCP-Protocol-Version": "2026-07-28", + "Mcp-Method": "tools/call", + "Mcp-Name": "get_community_stats" + } + }, + "response": { + "body": { + "id": "fixture-stats-call", + "jsonrpc": "2.0", + "result": { + "_meta": { + "io.modelcontextprotocol/serverInfo": { + "icons": [ + { + "mimeType": "image/png", + "sizes": [ + "180x180" + ], + "src": "https://agentcommunity.org/apple-touch-icon.png" + } + ], + "name": "agentcommunity", + "title": "Agent Community", + "version": "1.0.0", + "websiteUrl": "https://agentcommunity.org" + } + }, + "content": [ + { + "text": "{\"member_count\":0,\"note\":\"Fixture-only value; not production member-count evidence.\"}", + "type": "text" + } + ], + "resultType": "complete", + "structuredContent": { + "member_count": 0, + "note": "Fixture-only value; not production member-count evidence." + } + } + }, + "http_status": 200 + }, + "tool": "get_community_stats" + }, + { + "request": { + "body": { + "id": "fixture-certificate-call", + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "_meta": { + "io.modelcontextprotocol/clientCapabilities": {}, + "io.modelcontextprotocol/clientInfo": { + "name": "@agentcommunity/cli", + "version": "0.0.0-fixture" + }, + "io.modelcontextprotocol/protocolVersion": "2026-07-28" + }, + "arguments": { + "certificate_id": "FIXTURE-NOT-A-CERTIFICATE" + }, + "name": "verify_certificate" + } + }, + "endpoint": "/mcp", + "headers": { + "Accept": "application/json", + "Content-Type": "application/json", + "MCP-Protocol-Version": "2026-07-28", + "Mcp-Method": "tools/call", + "Mcp-Name": "verify_certificate" + } + }, + "response": { + "body": { + "id": "fixture-certificate-call", + "jsonrpc": "2.0", + "result": { + "_meta": { + "io.modelcontextprotocol/serverInfo": { + "icons": [ + { + "mimeType": "image/png", + "sizes": [ + "180x180" + ], + "src": "https://agentcommunity.org/apple-touch-icon.png" + } + ], + "name": "agentcommunity", + "title": "Agent Community", + "version": "1.0.0", + "websiteUrl": "https://agentcommunity.org" + } + }, + "content": [ + { + "text": "{\"certificate_id\":\"FIXTURE-NOT-A-CERTIFICATE\",\"status\":\"invalid_format\",\"valid_format\":false,\"issued\":false,\"agent_name\":null,\"certificate_url\":null}", + "type": "text" + } + ], + "resultType": "complete", + "structuredContent": { + "agent_name": null, + "certificate_id": "FIXTURE-NOT-A-CERTIFICATE", + "certificate_url": null, + "issued": false, + "status": "invalid_format", + "valid_format": false + } + } + }, + "http_status": 200 + }, + "tool": "verify_certificate" + } + ], + "tools_list": { + "request": { + "body": { + "id": "fixture-tools-list", + "jsonrpc": "2.0", + "method": "tools/list", + "params": { + "_meta": { + "io.modelcontextprotocol/clientCapabilities": {}, + "io.modelcontextprotocol/clientInfo": { + "name": "@agentcommunity/cli", + "version": "0.0.0-fixture" + }, + "io.modelcontextprotocol/protocolVersion": "2026-07-28" + } + } + }, + "endpoint": "/mcp", + "headers": { + "Accept": "application/json", + "Content-Type": "application/json", + "MCP-Protocol-Version": "2026-07-28", + "Mcp-Method": "tools/list" + } + }, + "response": { + "body": { + "id": "fixture-tools-list", + "jsonrpc": "2.0", + "result": { + "_meta": { + "io.modelcontextprotocol/serverInfo": { + "icons": [ + { + "mimeType": "image/png", + "sizes": [ + "180x180" + ], + "src": "https://agentcommunity.org/apple-touch-icon.png" + } + ], + "name": "agentcommunity", + "title": "Agent Community", + "version": "1.0.0", + "websiteUrl": "https://agentcommunity.org" + } + }, + "cacheScope": "public", + "resultType": "complete", + "tools": [ + { + "annotations": { + "destructiveHint": false, + "openWorldHint": false, + "readOnlyHint": true + }, + "description": "Look up one public member by exact display name or slug. This is not free-text directory search and returns only minimal public information.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "query": { + "description": "Exact display name or slug; not free-text search.", + "maxLength": 200, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "lookup_member", + "outputSchema": { + "additionalProperties": false, + "properties": { + "matches": { + "items": { + "additionalProperties": false, + "properties": { + "display_name": { + "type": "string" + }, + "member_since": { + "format": "date", + "type": [ + "string", + "null" + ] + }, + "profile_url": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "display_name", + "member_since", + "profile_url" + ], + "type": "object" + }, + "maxItems": 5, + "type": "array" + }, + "status": { + "enum": [ + "member", + "not_found", + "ambiguous" + ], + "type": "string" + } + }, + "required": [ + "status", + "matches" + ], + "type": "object" + }, + "title": "Look up member" + }, + { + "_meta": { + "ui": { + "resourceUri": "ui://agentcommunity/community-stats.html" + }, + "ui/resourceUri": "ui://agentcommunity/community-stats.html" + }, + "annotations": { + "destructiveHint": false, + "openWorldHint": false, + "readOnlyHint": true + }, + "description": "Get the roughly-live Agent Community member count. The count is cached for up to 20 minutes.", + "inputSchema": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "name": "get_community_stats", + "outputSchema": { + "additionalProperties": false, + "properties": { + "member_count": { + "type": "integer" + }, + "note": { + "type": "string" + } + }, + "required": [ + "member_count", + "note" + ], + "type": "object" + }, + "title": "Get community stats" + }, + { + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false + }, + "description": "Pre-register a .agent identity through the DMV. This is free and non-binding, does not confer domain ownership, and should run only after an explicit user request.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "agent_name": { + "description": "Desired name without the .agent suffix.", + "maxLength": 63, + "minLength": 3, + "pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$", + "type": "string" + }, + "description": { + "description": "Optional description.", + "maxLength": 500, + "type": "string" + }, + "email": { + "description": "Email address for confirmation.", + "format": "email", + "type": "string" + }, + "operator_name": { + "description": "Person or organization operating the agent.", + "maxLength": 100, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "agent_name", + "email", + "operator_name" + ], + "type": "object" + }, + "name": "register_agent", + "outputSchema": { + "additionalProperties": false, + "properties": { + "agent_name": { + "type": "string" + }, + "already_recorded": { + "type": "boolean" + }, + "badge_card_url": { + "format": "uri", + "type": "string" + }, + "badge_url": { + "format": "uri", + "type": "string" + }, + "certificate_id": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "message": { + "type": "string" + }, + "permalink_url": { + "format": "uri", + "type": "string" + }, + "registration_type": { + "type": "string" + } + }, + "required": [ + "certificate_id", + "agent_name", + "domain", + "registration_type", + "permalink_url", + "badge_url", + "badge_card_url", + "message" + ], + "type": "object" + }, + "title": "Pre-register .agent name" + }, + { + "annotations": { + "destructiveHint": false, + "openWorldHint": false, + "readOnlyHint": true + }, + "description": "Verify the live DMV issuance state of a canonical certificate ID. This does not confer domain ownership or mean the .agent TLD exists in DNS.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "certificate_id": { + "description": "Certificate ID, e.g. MESA-DD6-660J.", + "type": "string" + } + }, + "required": [ + "certificate_id" + ], + "type": "object" + }, + "name": "verify_certificate", + "outputSchema": { + "additionalProperties": false, + "properties": { + "agent_name": { + "type": [ + "string", + "null" + ] + }, + "certificate_id": { + "type": "string" + }, + "certificate_url": { + "format": "uri", + "type": [ + "string", + "null" + ] + }, + "issued": { + "type": [ + "boolean", + "null" + ] + }, + "status": { + "enum": [ + "invalid_format", + "not_found", + "issued", + "unavailable" + ], + "type": "string" + }, + "valid_format": { + "type": "boolean" + } + }, + "required": [ + "certificate_id", + "status", + "valid_format", + "issued", + "agent_name", + "certificate_url" + ], + "type": "object" + }, + "title": "Verify DMV certificate" + } + ], + "ttlMs": 300000 + } + }, + "http_status": 200 + } + } + } + }, + "product_tools": [ + { + "annotations": { + "destructiveHint": false, + "openWorldHint": false, + "readOnlyHint": true + }, + "description": "Look up one public member by exact display name or slug. This is not free-text directory search and returns only minimal public information.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "query": { + "description": "Exact display name or slug; not free-text search.", + "maxLength": 200, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "lookup_member", + "outputSchema": { + "additionalProperties": false, + "properties": { + "matches": { + "items": { + "additionalProperties": false, + "properties": { + "display_name": { + "type": "string" + }, + "member_since": { + "format": "date", + "type": [ + "string", + "null" + ] + }, + "profile_url": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "display_name", + "member_since", + "profile_url" + ], + "type": "object" + }, + "maxItems": 5, + "type": "array" + }, + "status": { + "enum": [ + "member", + "not_found", + "ambiguous" + ], + "type": "string" + } + }, + "required": [ + "status", + "matches" + ], + "type": "object" + }, + "title": "Look up member" + }, + { + "_meta": { + "ui": { + "resourceUri": "ui://agentcommunity/community-stats.html" + }, + "ui/resourceUri": "ui://agentcommunity/community-stats.html" + }, + "annotations": { + "destructiveHint": false, + "openWorldHint": false, + "readOnlyHint": true + }, + "description": "Get the roughly-live Agent Community member count. The count is cached for up to 20 minutes.", + "inputSchema": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "name": "get_community_stats", + "outputSchema": { + "additionalProperties": false, + "properties": { + "member_count": { + "type": "integer" + }, + "note": { + "type": "string" + } + }, + "required": [ + "member_count", + "note" + ], + "type": "object" + }, + "title": "Get community stats" + }, + { + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false + }, + "description": "Pre-register a .agent identity through the DMV. This is free and non-binding, does not confer domain ownership, and should run only after an explicit user request.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "agent_name": { + "description": "Desired name without the .agent suffix.", + "maxLength": 63, + "minLength": 3, + "pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$", + "type": "string" + }, + "description": { + "description": "Optional description.", + "maxLength": 500, + "type": "string" + }, + "email": { + "description": "Email address for confirmation.", + "format": "email", + "type": "string" + }, + "operator_name": { + "description": "Person or organization operating the agent.", + "maxLength": 100, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "agent_name", + "email", + "operator_name" + ], + "type": "object" + }, + "name": "register_agent", + "outputSchema": { + "additionalProperties": false, + "properties": { + "agent_name": { + "type": "string" + }, + "already_recorded": { + "type": "boolean" + }, + "badge_card_url": { + "format": "uri", + "type": "string" + }, + "badge_url": { + "format": "uri", + "type": "string" + }, + "certificate_id": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "message": { + "type": "string" + }, + "permalink_url": { + "format": "uri", + "type": "string" + }, + "registration_type": { + "type": "string" + } + }, + "required": [ + "certificate_id", + "agent_name", + "domain", + "registration_type", + "permalink_url", + "badge_url", + "badge_card_url", + "message" + ], + "type": "object" + }, + "title": "Pre-register .agent name" + }, + { + "annotations": { + "destructiveHint": false, + "openWorldHint": false, + "readOnlyHint": true + }, + "description": "Verify the live DMV issuance state of a canonical certificate ID. This does not confer domain ownership or mean the .agent TLD exists in DNS.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "certificate_id": { + "description": "Certificate ID, e.g. MESA-DD6-660J.", + "type": "string" + } + }, + "required": [ + "certificate_id" + ], + "type": "object" + }, + "name": "verify_certificate", + "outputSchema": { + "additionalProperties": false, + "properties": { + "agent_name": { + "type": [ + "string", + "null" + ] + }, + "certificate_id": { + "type": "string" + }, + "certificate_url": { + "format": "uri", + "type": [ + "string", + "null" + ] + }, + "issued": { + "type": [ + "boolean", + "null" + ] + }, + "status": { + "enum": [ + "invalid_format", + "not_found", + "issued", + "unavailable" + ], + "type": "string" + }, + "valid_format": { + "type": "boolean" + } + }, + "required": [ + "certificate_id", + "status", + "valid_format", + "issued", + "agent_name", + "certificate_url" + ], + "type": "object" + }, + "title": "Verify DMV certificate" + } + ], + "supported_revisions": [ + "2026-07-28", + "2025-11-25", + "2025-06-18", + "2025-03-26", + "2024-11-05" + ] +} diff --git a/contracts/page/1.0.0/openapi.json b/contracts/page/1.0.0/openapi.json new file mode 100644 index 0000000..50f0cfe --- /dev/null +++ b/contracts/page/1.0.0/openapi.json @@ -0,0 +1,4960 @@ +{ + "components": { + "schemas": { + "AgentAccessTokenRevocation": { + "additionalProperties": false, + "properties": { + "token": { + "minLength": 1, + "type": "string" + }, + "token_type_hint": { + "type": "string" + } + }, + "required": [ + "token" + ], + "type": "object" + }, + "AgentApiError": { + "additionalProperties": false, + "properties": { + "error": { + "enum": [ + "invalid_request", + "unauthorized", + "forbidden", + "rate_limited", + "service_unavailable" + ], + "type": "string" + } + }, + "required": [ + "error" + ], + "type": "object" + }, + "AgentAuthError": { + "additionalProperties": false, + "properties": { + "error": { + "type": "string" + }, + "error_description": { + "type": "string" + } + }, + "required": [ + "error", + "error_description" + ], + "type": "object" + }, + "AgentAuthorizationEnvelope": { + "additionalProperties": false, + "properties": { + "access_token_expires_at": { + "format": "date-time", + "type": "string" + }, + "delegation_expires_at": { + "format": "date-time", + "type": "string" + }, + "registration_id": { + "format": "uuid", + "type": "string" + }, + "scopes": { + "items": { + "enum": [ + "agent.account.read", + "agent.registrations.read" + ], + "type": "string" + }, + "maxItems": 2, + "minItems": 1, + "type": "array", + "uniqueItems": true + }, + "status": { + "const": "approved", + "type": "string" + } + }, + "required": [ + "registration_id", + "status", + "scopes", + "access_token_expires_at", + "delegation_expires_at" + ], + "type": "object" + }, + "AgentOwnAccountResponse": { + "additionalProperties": false, + "properties": { + "account": { + "additionalProperties": false, + "properties": { + "email": { + "format": "email", + "type": "string" + }, + "email_verified": { + "const": true, + "type": "boolean" + }, + "id": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "id", + "email", + "email_verified" + ], + "type": "object" + }, + "authorization": { + "$ref": "#/components/schemas/AgentAuthorizationEnvelope" + } + }, + "required": [ + "account", + "authorization" + ], + "type": "object" + }, + "AgentOwnRegistrationsResponse": { + "additionalProperties": false, + "properties": { + "registrations": { + "items": { + "additionalProperties": false, + "properties": { + "id": { + "format": "uuid", + "type": "string" + }, + "registration_type": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "required": [ + "id", + "registration_type", + "status" + ], + "type": "object" + }, + "maxItems": 1, + "type": "array" + } + }, + "required": [ + "registrations" + ], + "type": "object" + }, + "ApprovedClaimTokenResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/AssertionExchangeResponse" + }, + { + "additionalProperties": false, + "properties": { + "access_token": { + "pattern": "^aca_[A-Za-z0-9_-]{43}$", + "type": "string" + }, + "assertion_expires": { + "format": "date-time", + "type": "string" + }, + "expires_in": { + "maximum": 3600, + "minimum": 1, + "type": "integer" + }, + "identity_assertion": { + "maxLength": 8192, + "minLength": 1, + "type": "string" + }, + "scope": { + "type": "string" + }, + "token_type": { + "const": "Bearer", + "type": "string" + } + }, + "required": [ + "access_token", + "token_type", + "expires_in", + "scope", + "identity_assertion", + "assertion_expires" + ], + "type": "object" + } + ] + }, + "AssertionExchangeResponse": { + "additionalProperties": false, + "properties": { + "access_token": { + "pattern": "^aca_[A-Za-z0-9_-]{43}$", + "type": "string" + }, + "expires_in": { + "maximum": 3600, + "minimum": 1, + "type": "integer" + }, + "scope": { + "type": "string" + }, + "token_type": { + "const": "Bearer", + "type": "string" + } + }, + "required": [ + "access_token", + "token_type", + "expires_in", + "scope" + ], + "type": "object" + }, + "AssertionGrant": { + "additionalProperties": false, + "properties": { + "assertion": { + "maxLength": 8192, + "minLength": 1, + "type": "string" + }, + "grant_type": { + "const": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "type": "string" + }, + "resource": { + "const": "https://agentcommunity.org/api", + "type": "string" + } + }, + "required": [ + "grant_type", + "assertion", + "resource" + ], + "type": "object" + }, + "BatchEnvelopeItem": { + "additionalProperties": false, + "properties": { + "arguments": {}, + "id": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "type": "string" + }, + "operation": { + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "operation", + "arguments" + ], + "type": "object" + }, + "BatchKnownItem": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "arguments": { + "additionalProperties": false, + "properties": { + "cursor": { + "maxLength": 256, + "type": "string" + }, + "limit": { + "default": 20, + "maximum": 50, + "minimum": 1, + "type": "integer" + }, + "query": { + "maxLength": 200, + "type": "string" + }, + "type": { + "enum": [ + "docs", + "blog", + "page" + ], + "type": "string" + } + }, + "type": "object" + }, + "id": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "type": "string" + }, + "operation": { + "const": "content.list", + "type": "string" + } + }, + "required": [ + "id", + "operation", + "arguments" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "arguments": { + "additionalProperties": false, + "properties": { + "query": { + "maxLength": 500, + "minLength": 2, + "type": "string" + }, + "top_k": { + "default": 5, + "maximum": 10, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "id": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "type": "string" + }, + "operation": { + "const": "docs.ask", + "type": "string" + } + }, + "required": [ + "id", + "operation", + "arguments" + ], + "type": "object" + } + ] + }, + "BatchRequest": { + "additionalProperties": false, + "properties": { + "items": { + "items": { + "additionalProperties": false, + "properties": { + "arguments": {}, + "id": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "type": "string" + }, + "operation": { + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "operation", + "arguments" + ], + "type": "object" + }, + "maxItems": 10, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "items" + ], + "type": "object" + }, + "BatchResponse": { + "additionalProperties": false, + "properties": { + "items": { + "items": { + "anyOf": [ + { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "id": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "type": "string" + }, + "operation": { + "const": "content.list", + "type": "string" + }, + "result": { + "additionalProperties": false, + "properties": { + "items": { + "items": { + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "href": { + "pattern": "^\\/(?!\\/)", + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "docs", + "blog", + "page" + ], + "type": "string" + } + }, + "required": [ + "title", + "description", + "href", + "type" + ], + "type": "object" + }, + "maxItems": 50, + "type": "array" + }, + "page": { + "additionalProperties": false, + "properties": { + "has_more": { + "type": "boolean" + }, + "limit": { + "maximum": 50, + "minimum": 1, + "type": "integer" + }, + "next_cursor": { + "anyOf": [ + { + "maxLength": 256, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "limit", + "next_cursor", + "has_more" + ], + "type": "object" + } + }, + "required": [ + "items", + "page" + ], + "type": "object" + }, + "status": { + "const": "ok", + "type": "string" + } + }, + "required": [ + "id", + "operation", + "status", + "result" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "id": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "type": "string" + }, + "operation": { + "const": "docs.ask", + "type": "string" + }, + "result": { + "additionalProperties": false, + "properties": { + "answer": { + "maxLength": 1500, + "type": "string" + }, + "query": { + "maxLength": 500, + "type": "string" + }, + "sources": { + "items": { + "additionalProperties": false, + "properties": { + "description": { + "maxLength": 500, + "type": "string" + }, + "excerpt": { + "maxLength": 320, + "type": "string" + }, + "path": { + "pattern": "^\\/(?!\\/)", + "type": "string" + }, + "title": { + "maxLength": 200, + "type": "string" + }, + "url": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "title", + "description", + "path", + "url", + "excerpt" + ], + "type": "object" + }, + "maxItems": 10, + "type": "array" + } + }, + "required": [ + "query", + "answer", + "sources" + ], + "type": "object" + }, + "status": { + "const": "ok", + "type": "string" + } + }, + "required": [ + "id", + "operation", + "status", + "result" + ], + "type": "object" + } + ] + }, + { + "additionalProperties": false, + "properties": { + "error": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "code": { + "const": "unknown_operation", + "type": "string" + }, + "message": { + "const": "Unsupported operation", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "invalid_arguments", + "type": "string" + }, + "message": { + "const": "Invalid arguments", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "operation_failed", + "type": "string" + }, + "message": { + "const": "Operation failed", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "deadline_exceeded", + "type": "string" + }, + "message": { + "const": "Batch deadline exceeded", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "response_too_large", + "type": "string" + }, + "message": { + "const": "Operation response exceeds its limit", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "total_response_too_large", + "type": "string" + }, + "message": { + "const": "Batch response budget exceeded", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + } + ] + }, + "id": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "type": "string" + }, + "operation": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "status": { + "const": "error", + "type": "string" + } + }, + "required": [ + "id", + "operation", + "status", + "error" + ], + "type": "object" + } + ] + }, + "maxItems": 10, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "items" + ], + "type": "object" + }, + "BatchRouteError": { + "additionalProperties": false, + "properties": { + "error": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "code": { + "const": "invalid_json", + "type": "string" + }, + "message": { + "const": "Invalid JSON request body", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "invalid_request", + "type": "string" + }, + "message": { + "const": "Invalid batch request", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "request_too_large", + "type": "string" + }, + "message": { + "const": "Batch request exceeds 262144 bytes", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "unsupported_media_type", + "type": "string" + }, + "message": { + "const": "Content-Type must be application/json", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "rate_limited", + "type": "string" + }, + "message": { + "const": "Too many batch requests", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "internal_error", + "type": "string" + }, + "message": { + "const": "Batch execution failed", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + } + ] + } + }, + "required": [ + "error" + ], + "type": "object" + }, + "ClaimCeremony": { + "additionalProperties": false, + "properties": { + "expires_in": { + "const": 600, + "type": "integer" + }, + "interval": { + "const": 5, + "type": "integer" + }, + "user_code": { + "pattern": "^\\d{6}$", + "type": "string" + }, + "verification_uri": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "user_code", + "expires_in", + "verification_uri", + "interval" + ], + "type": "object" + }, + "ClaimRefreshRequest": { + "additionalProperties": false, + "properties": { + "claim_token": { + "pattern": "^clm_[A-Za-z0-9_-]{43}$", + "type": "string" + }, + "email": { + "format": "email", + "maxLength": 320, + "type": "string" + } + }, + "required": [ + "claim_token", + "email" + ], + "type": "object" + }, + "ClaimRefreshResponse": { + "additionalProperties": false, + "properties": { + "claim_attempt": { + "$ref": "#/components/schemas/ClaimCeremony" + }, + "claim_attempt_id": { + "format": "uuid", + "type": "string" + }, + "expires_at": { + "format": "date-time", + "type": "string" + }, + "registration_id": { + "format": "uuid", + "type": "string" + }, + "status": { + "const": "initiated", + "type": "string" + } + }, + "required": [ + "registration_id", + "claim_attempt_id", + "status", + "expires_at", + "claim_attempt" + ], + "type": "object" + }, + "ClaimTokenGrant": { + "additionalProperties": false, + "properties": { + "claim_token": { + "pattern": "^clm_[A-Za-z0-9_-]{43}$", + "type": "string" + }, + "grant_type": { + "const": "urn:workos:agent-auth:grant-type:claim", + "type": "string" + } + }, + "required": [ + "grant_type", + "claim_token" + ], + "type": "object" + }, + "ContentPage": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/ContentSearchResult" + }, + "type": "array" + }, + "page": { + "$ref": "#/components/schemas/CursorPage" + } + }, + "required": [ + "items", + "page" + ], + "type": "object" + }, + "ContentSearchResult": { + "properties": { + "description": { + "type": "string" + }, + "href": { + "description": "Site-relative path, e.g. /docs/getting-started or /blog/some-post.", + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "docs", + "blog", + "page" + ], + "type": "string" + } + }, + "required": [ + "title", + "description", + "href", + "type" + ], + "type": "object" + }, + "CursorPage": { + "properties": { + "has_more": { + "type": "boolean" + }, + "limit": { + "maximum": 50, + "minimum": 1, + "type": "integer" + }, + "next_cursor": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "limit", + "next_cursor", + "has_more" + ], + "type": "object" + }, + "Error": { + "properties": { + "error": { + "description": "Machine-readable error code, e.g. query_too_short, rate_limited, not_found.", + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "error" + ], + "type": "object" + }, + "GlobePoint": { + "properties": { + "count": { + "description": "Number of member organizations aggregated into this point (city cluster or country-jitter cell).", + "type": "integer" + }, + "lat": { + "type": "number" + }, + "lng": { + "type": "number" + } + }, + "required": [ + "lat", + "lng", + "count" + ], + "type": "object" + }, + "NLWebAnswer": { + "additionalProperties": false, + "properties": { + "_meta": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "list", + "type": "string" + }, + "response_type": { + "description": "answer for a normal query; capability for the parameterless probe response.", + "enum": [ + "answer", + "capability" + ], + "type": "string" + }, + "site": { + "const": "agentcommunity.org", + "type": "string" + }, + "version": { + "const": "0.55", + "type": "string" + } + }, + "required": [ + "version", + "response_type", + "mode", + "site" + ], + "type": "object" + }, + "answer": { + "description": "Deterministic extractive answer assembled from cited documentation. Each internal source excerpt is bounded to 320 characters before the full answer is capped.", + "maxLength": 1500, + "type": "string" + }, + "content": { + "items": { + "additionalProperties": false, + "properties": { + "@context": { + "const": "https://schema.org", + "type": "string" + }, + "@id": { + "format": "uri", + "type": "string" + }, + "@type": { + "const": "Article", + "type": "string" + }, + "description": { + "maxLength": 500, + "type": "string" + }, + "name": { + "maxLength": 200, + "type": "string" + }, + "url": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "@context", + "@type", + "@id", + "name", + "description", + "url" + ], + "type": "object" + }, + "maxItems": 10, + "type": "array" + }, + "query": { + "description": "Echo of the caller query; empty string in a capability response.", + "maxLength": 500, + "type": "string" + }, + "results": { + "description": "Canonical NLWeb result list. Array order is the ranking; no relevance score is published.", + "items": { + "additionalProperties": false, + "properties": { + "description": { + "maxLength": 500, + "type": "string" + }, + "name": { + "maxLength": 200, + "type": "string" + }, + "schema_object": { + "additionalProperties": false, + "properties": { + "@context": { + "const": "https://schema.org", + "type": "string" + }, + "@id": { + "format": "uri", + "type": "string" + }, + "@type": { + "const": "Article", + "type": "string" + }, + "description": { + "maxLength": 500, + "type": "string" + }, + "name": { + "maxLength": 200, + "type": "string" + }, + "url": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "@context", + "@type", + "@id", + "name", + "description", + "url" + ], + "type": "object" + }, + "site": { + "const": "agentcommunity.org", + "type": "string" + }, + "url": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "url", + "site", + "name", + "description", + "schema_object" + ], + "type": "object" + }, + "maxItems": 10, + "type": "array" + } + }, + "required": [ + "_meta", + "query", + "answer", + "results", + "content" + ], + "type": "object" + }, + "SandboxRouteError": { + "additionalProperties": false, + "properties": { + "error": { + "enum": [ + "invalid_request", + "request_too_large", + "unsupported_media_type", + "rate_limited", + "simulation_unavailable" + ], + "type": "string" + } + }, + "required": [ + "error" + ], + "type": "object" + }, + "SandboxSimulationRequest": { + "additionalProperties": false, + "properties": { + "scenario": { + "enum": [ + "agent_auth_challenge", + "agent_auth_pending", + "agent_auth_success", + "batch_mixed_result" + ], + "type": "string" + } + }, + "required": [ + "scenario" + ], + "type": "object" + }, + "ServiceAuthRegistrationRequest": { + "additionalProperties": false, + "properties": { + "client_name": { + "maxLength": 120, + "minLength": 1, + "type": "string" + }, + "login_hint": { + "format": "email", + "maxLength": 320, + "type": "string" + }, + "scopes": { + "default": [ + "agent.account.read", + "agent.registrations.read" + ], + "items": { + "enum": [ + "agent.account.read", + "agent.registrations.read" + ], + "type": "string" + }, + "maxItems": 2, + "minItems": 1, + "type": "array", + "uniqueItems": true + }, + "type": { + "const": "service_auth", + "type": "string" + } + }, + "required": [ + "type", + "login_hint" + ], + "type": "object" + }, + "ServiceAuthRegistrationResponse": { + "additionalProperties": false, + "properties": { + "claim": { + "$ref": "#/components/schemas/ClaimCeremony" + }, + "claim_token": { + "pattern": "^clm_[A-Za-z0-9_-]{43}$", + "type": "string" + }, + "claim_token_expires": { + "format": "date-time", + "type": "string" + }, + "claim_url": { + "const": "https://agentcommunity.org/agent/identity/claim", + "type": "string" + }, + "post_claim_scopes": { + "items": { + "enum": [ + "agent.account.read", + "agent.registrations.read" + ], + "type": "string" + }, + "type": "array" + }, + "registration_id": { + "format": "uuid", + "type": "string" + }, + "registration_type": { + "const": "service_auth", + "type": "string" + } + }, + "required": [ + "registration_id", + "registration_type", + "claim_url", + "claim_token", + "claim_token_expires", + "post_claim_scopes", + "claim" + ], + "type": "object" + } + }, + "securitySchemes": { + "agentBearer": { + "bearerFormat": "opaque aca_ access token", + "description": "Header-only reusable access token for scoped own-account reads.", + "scheme": "bearer", + "type": "http" + }, + "supabaseOAuth": { + "description": "OAuth 2.0 authorization-code flow (PKCE S256) via Supabase Auth. No API tokens are issued for the public endpoints in this spec; this scheme documents the human sign-in flow only. Each scope below is described with the exact data it exposes — none of them authorize a call to any path in this document, which is why every operation carries security: [].", + "flows": { + "authorizationCode": { + "authorizationUrl": "https://tcymqfwwphacnosnnzxl.supabase.co/auth/v1/oauth/authorize", + "scopes": { + "email": "Read the signed-in person's email address and its verification state. Grants no read or write access to any API in this document.", + "openid": "Issue an ID token asserting the signed-in subject identifier. Grants no read or write access to any API in this document.", + "profile": "Read the signed-in person's display name and avatar. Grants no read or write access to any API in this document." + }, + "tokenUrl": "https://tcymqfwwphacnosnnzxl.supabase.co/auth/v1/oauth/token" + } + }, + "type": "oauth2" + }, + "supabaseOidc": { + "description": "Human browser OpenID Connect via the canonical Supabase issuer. Member-only surfaces use cookie sessions established through this flow. This is not Agent Community OIDC discovery and grants no capability on the public operations in this document.", + "openIdConnectUrl": "https://tcymqfwwphacnosnnzxl.supabase.co/auth/v1/.well-known/openid-configuration", + "type": "openIdConnect" + } + } + }, + "info": { + "contact": { + "email": "hello@agentcommunity.org", + "name": "Agent Community", + "url": "https://agentcommunity.org/contact" + }, + "description": "Public and explicitly scoped read APIs for agentcommunity.org — an open community of the companies, researchers, and developers building the agentic web, and the applicant for the proposed .agent top-level domain (pending ICANN approval). Includes globe data, site content search, deterministic NLWeb-compatible public-documentation answers at /ask, a hosted MCP server at /mcp, and two Bearer-protected own-account resources. Exact member lookup is provided through MCP; broad directory access is not a supported anonymous integration. Anonymous operations declare security: []; own-account operations declare their exact agentBearer scope. API versioning: public REST endpoints are versioned by URL path (/api/v1/). Breaking changes ship as a new version prefix; prior versions are intended to remain available for a transition period (target: at least 6 months) after a successor ships, with retirement announced in the spec via deprecated: true. Unversioned legacy paths are deprecated aliases of /api/v1.", + "title": "Agent Community Public API", + "version": "1.3.0" + }, + "openapi": "3.1.0", + "paths": { + "/.well-known/jwks.json": { + "get": { + "description": "Public Ed25519 verification keys for signed agent authorization assertions. Private signing material is never exposed.", + "operationId": "getAgentAuthorizationServerJwks", + "responses": { + "200": { + "content": { + "application/jwk-set+json": { + "schema": { + "properties": { + "keys": { + "items": { + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "keys" + ], + "type": "object" + } + } + }, + "description": "Public JSON Web Key Set." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Error" + } + }, + "security": [], + "summary": "Read Agent Community agent authorization public keys" + } + }, + "/.well-known/oauth-authorization-server": { + "get": { + "description": "Token-only WorkOS auth.md profile metadata composed with IETF OAuth standards. response_types_supported is deliberately omitted because no authorization response type exists.", + "operationId": "getAgentAuthorizationServerMetadata", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "Agent authorization-server metadata." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Error" + } + }, + "security": [], + "summary": "Discover the Agent Community agent authorization server" + } + }, + "/.well-known/oauth-protected-resource/api": { + "get": { + "description": "RFC 9728 metadata for the exact protected resource https://agentcommunity.org/api. This discovery operation is public; the protected resource itself is separate.", + "operationId": "getAgentApiProtectedResourceMetadata", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "Protected-resource metadata." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Error" + } + }, + "security": [], + "summary": "Discover authorization for the protected Agent API resource" + } + }, + "/agent/identity": { + "post": { + "description": "Starts the WorkOS service_auth ceremony. Strict JSON, 16 KiB maximum. Accepts absent Origin or exactly https://agentcommunity.org; no CORS or OPTIONS extension.", + "operationId": "startServiceAuthorization", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceAuthRegistrationRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceAuthRegistrationResponse" + } + } + }, + "description": "One-time outer claim token and initial ten-minute claim attempt.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-auth-start-ip\";q=5;w=60, \"agent-auth-start-hint\";q=3;w=60" + ], + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Malformed or non-canonical registration request.", + "headers": {} + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Present Origin was not the exact allowed origin.", + "headers": {} + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Authorization runtime is disabled.", + "headers": {} + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Request body exceeds 16 KiB.", + "headers": {} + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Content-Type is not application/json with optional UTF-8 charset.", + "headers": {} + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Trusted-IP or normalized-login-hint start limit rejected the request.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-auth-start-ip\";q=5;w=60, \"agent-auth-start-hint\";q=3;w=60" + ], + "type": "string" + } + } + } + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Authorization service is unavailable.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-auth-start-ip\";q=5;w=60, \"agent-auth-start-hint\";q=3;w=60" + ], + "type": "string" + } + } + } + } + }, + "security": [], + "summary": "Start user-claimed service authorization" + } + }, + "/agent/identity/claim": { + "post": { + "description": "Atomically closes the current attempt and creates a fresh ten-minute attempt while the outer claim remains active. It never returns the outer claim token.", + "operationId": "refreshServiceAuthorizationClaim", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClaimRefreshRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClaimRefreshResponse" + } + } + }, + "description": "Fresh initiated claim attempt.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-auth-start-ip\";q=5;w=60, \"agent-auth-start-hint\";q=3;w=60" + ], + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Malformed request.", + "headers": {} + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "invalid_claim_token.", + "headers": {} + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Present Origin was not the exact allowed origin.", + "headers": {} + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Authorization runtime is disabled.", + "headers": {} + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "claimed_or_in_flight.", + "headers": {} + }, + "410": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "claim_expired.", + "headers": {} + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Request body exceeds 16 KiB.", + "headers": {} + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Content-Type is not application/json with optional UTF-8 charset.", + "headers": {} + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Trusted-IP or normalized-login-hint start limit rejected the request.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-auth-start-ip\";q=5;w=60, \"agent-auth-start-hint\";q=3;w=60" + ], + "type": "string" + } + } + } + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Authorization service is unavailable.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-auth-start-ip\";q=5;w=60, \"agent-auth-start-hint\";q=3;w=60" + ], + "type": "string" + } + } + } + } + }, + "security": [], + "summary": "Rotate the active claim attempt" + } + }, + "/api/content-search": { + "get": { + "deprecated": true, + "description": "Unversioned alias of /api/v1/content-search. Still supported; new integrations should use /api/v1/content-search. Deprecated for new integrations: use /api/v1/content?q=... for deterministic cursor pagination.", + "operationId": "searchContentLegacy", + "parameters": [ + { + "description": "Search query.", + "in": "query", + "name": "q", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ContentSearchResult" + }, + "type": "array" + } + } + }, + "description": "Content search results." + } + }, + "security": [], + "summary": "Search site content (legacy alias)" + } + }, + "/api/map/globe": { + "get": { + "deprecated": true, + "description": "Unversioned alias of /api/v1/globe. Still supported; new integrations should use /api/v1/globe.", + "operationId": "getGlobeDataLegacy", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "countries": { + "type": "integer" + }, + "plottedOrgs": { + "type": "integer" + }, + "points": { + "items": { + "$ref": "#/components/schemas/GlobePoint" + }, + "type": "array" + }, + "totalOrgs": { + "type": "integer" + } + }, + "type": "object" + } + } + }, + "description": "Globe points and totals." + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Error" + } + }, + "security": [], + "summary": "Member globe data (legacy alias)" + } + }, + "/api/v1/agent/account": { + "get": { + "description": "Returns only the verified auth account and the current delegation authorization envelope. Accepts no query parameters or request body.", + "operationId": "getOwnAgentAccount", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentOwnAccountResponse" + } + } + }, + "description": "Verified own-account projection.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-api\";q=60;w=60" + ], + "type": "string" + } + }, + "X-RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentApiError" + } + } + }, + "description": "Invalid request transport, including query parameters, body signals, or multiple credentials.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-api\";q=60;w=60" + ], + "type": "string" + } + }, + "WWW-Authenticate": { + "description": "RFC 6750 Bearer challenge. Child resources omit resource_metadata and error descriptions.", + "schema": { + "type": "string" + } + }, + "X-RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentApiError" + } + } + }, + "description": "Missing, malformed, invalid, expired, revoked, or incorrectly bound access token.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-api\";q=60;w=60" + ], + "type": "string" + } + }, + "WWW-Authenticate": { + "description": "RFC 6750 Bearer challenge. Child resources omit resource_metadata and error descriptions.", + "schema": { + "type": "string" + } + }, + "X-RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentApiError" + } + } + }, + "description": "Foreign Origin or insufficient route scope.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-api\";q=60;w=60" + ], + "type": "string" + } + }, + "WWW-Authenticate": { + "description": "RFC 6750 Bearer challenge. Child resources omit resource_metadata and error descriptions.", + "schema": { + "type": "string" + } + }, + "X-RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + } + } + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentApiError" + } + } + }, + "description": "Fail-closed token rate limit rejection.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-api\";q=60;w=60" + ], + "type": "string" + } + }, + "Retry-After": { + "schema": { + "const": "60", + "type": "string" + } + }, + "X-RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + } + } + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentApiError" + } + } + }, + "description": "Authorization repository, account reader, or server configuration unavailable.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-api\";q=60;w=60" + ], + "type": "string" + } + }, + "X-RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "agentBearer": [ + "agent.account.read" + ] + } + ], + "summary": "Read the access token owner account" + } + }, + "/api/v1/agent/registrations": { + "get": { + "description": "Returns zero or one registration selected only by the authenticated user ID. Accepts no search, filter, pagination, query parameters, or request body.", + "operationId": "getOwnAgentRegistrations", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentOwnRegistrationsResponse" + } + } + }, + "description": "Safe own-registration projection.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-api\";q=60;w=60" + ], + "type": "string" + } + }, + "X-RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentApiError" + } + } + }, + "description": "Invalid request transport, including query parameters, body signals, or multiple credentials.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-api\";q=60;w=60" + ], + "type": "string" + } + }, + "WWW-Authenticate": { + "description": "RFC 6750 Bearer challenge. Child resources omit resource_metadata and error descriptions.", + "schema": { + "type": "string" + } + }, + "X-RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentApiError" + } + } + }, + "description": "Missing, malformed, invalid, expired, revoked, or incorrectly bound access token.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-api\";q=60;w=60" + ], + "type": "string" + } + }, + "WWW-Authenticate": { + "description": "RFC 6750 Bearer challenge. Child resources omit resource_metadata and error descriptions.", + "schema": { + "type": "string" + } + }, + "X-RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentApiError" + } + } + }, + "description": "Foreign Origin or insufficient route scope.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-api\";q=60;w=60" + ], + "type": "string" + } + }, + "WWW-Authenticate": { + "description": "RFC 6750 Bearer challenge. Child resources omit resource_metadata and error descriptions.", + "schema": { + "type": "string" + } + }, + "X-RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + } + } + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentApiError" + } + } + }, + "description": "Fail-closed token rate limit rejection.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-api\";q=60;w=60" + ], + "type": "string" + } + }, + "Retry-After": { + "schema": { + "const": "60", + "type": "string" + } + }, + "X-RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + } + } + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentApiError" + } + } + }, + "description": "Authorization repository, account reader, or server configuration unavailable.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-api\";q=60;w=60" + ], + "type": "string" + } + }, + "X-RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "agentBearer": [ + "agent.registrations.read" + ] + } + ], + "summary": "Read the access token owner registration" + } + }, + "/api/v1/batch": { + "post": { + "description": "Anonymous JSON batch for content.list and docs.ask. One request carries at most 10 operations and at most 262144 bytes; each current operation costs 1 against a total cost budget of 10. Execution is sequential with concurrency one. The content.list item response cap is 262144 bytes, the docs.ask item response cap is 65536 bytes, and the total encoded response cap is 1048576 bytes. The trusted-IP policy allows 10 requests per 60 seconds. The 10-second batch execution deadline begins when validated execution starts, and no later operation begins once that deadline is reached. Work already running may finish. Envelope failures reject the request; operation failures remain ordered item-level errors in an HTTP 200 response, never 207.", + "operationId": "executePublicReadBatch", + "requestBody": { + "content": { + "application/json": { + "example": { + "items": [ + { + "arguments": { + "limit": 5, + "query": "agent discovery", + "type": "docs" + }, + "id": "content-1", + "operation": "content.list" + }, + { + "arguments": { + "query": "How can agents discover AgentCommunity resources?", + "top_k": 3 + }, + "id": "docs-1", + "operation": "docs.ask" + } + ] + }, + "schema": { + "$ref": "#/components/schemas/BatchRequest" + } + } + }, + "description": "Strict batch envelope. Item IDs must be unique even though JSON Schema uniqueItems cannot express uniqueness by one object property. Unknown operation names remain envelope-valid and produce item-level errors; the closed known-operation argument union is BatchKnownItem.", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchResponse" + } + } + }, + "description": "Ordered per-item success and closed error results, including mixed outcomes.", + "headers": { + "Access-Control-Allow-Origin": { + "schema": { + "const": "*", + "type": "string" + } + }, + "Access-Control-Expose-Headers": { + "schema": { + "const": "RateLimit-Policy, RateLimit-Limit, X-RateLimit-Limit, Retry-After", + "type": "string" + } + }, + "Cache-Control": { + "schema": { + "const": "no-store", + "type": "string" + } + }, + "Pragma": { + "schema": { + "const": "no-cache", + "type": "string" + } + }, + "RateLimit-Limit": { + "schema": { + "const": "10", + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Static draft-11 named policy. No remaining or reset state is claimed.", + "schema": { + "const": "\"agent-batch\";q=10;w=60", + "type": "string" + } + }, + "X-Content-Type-Options": { + "schema": { + "const": "nosniff", + "type": "string" + } + }, + "X-RateLimit-Limit": { + "schema": { + "const": "10", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchRouteError" + } + } + }, + "description": "Malformed JSON or invalid strict batch envelope.", + "headers": { + "Access-Control-Allow-Origin": { + "schema": { + "const": "*", + "type": "string" + } + }, + "Access-Control-Expose-Headers": { + "schema": { + "const": "RateLimit-Policy, RateLimit-Limit, X-RateLimit-Limit, Retry-After", + "type": "string" + } + }, + "Cache-Control": { + "schema": { + "const": "no-store", + "type": "string" + } + }, + "Pragma": { + "schema": { + "const": "no-cache", + "type": "string" + } + }, + "RateLimit-Limit": { + "schema": { + "const": "10", + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Static draft-11 named policy. No remaining or reset state is claimed.", + "schema": { + "const": "\"agent-batch\";q=10;w=60", + "type": "string" + } + }, + "X-Content-Type-Options": { + "schema": { + "const": "nosniff", + "type": "string" + } + }, + "X-RateLimit-Limit": { + "schema": { + "const": "10", + "type": "string" + } + } + } + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchRouteError" + } + } + }, + "description": "Request body exceeds 262144 bytes.", + "headers": { + "Access-Control-Allow-Origin": { + "schema": { + "const": "*", + "type": "string" + } + }, + "Access-Control-Expose-Headers": { + "schema": { + "const": "RateLimit-Policy, RateLimit-Limit, X-RateLimit-Limit, Retry-After", + "type": "string" + } + }, + "Cache-Control": { + "schema": { + "const": "no-store", + "type": "string" + } + }, + "Pragma": { + "schema": { + "const": "no-cache", + "type": "string" + } + }, + "RateLimit-Limit": { + "schema": { + "const": "10", + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Static draft-11 named policy. No remaining or reset state is claimed.", + "schema": { + "const": "\"agent-batch\";q=10;w=60", + "type": "string" + } + }, + "X-Content-Type-Options": { + "schema": { + "const": "nosniff", + "type": "string" + } + }, + "X-RateLimit-Limit": { + "schema": { + "const": "10", + "type": "string" + } + } + } + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchRouteError" + } + } + }, + "description": "Content-Type is not application/json or Content-Encoding is not identity.", + "headers": { + "Access-Control-Allow-Origin": { + "schema": { + "const": "*", + "type": "string" + } + }, + "Access-Control-Expose-Headers": { + "schema": { + "const": "RateLimit-Policy, RateLimit-Limit, X-RateLimit-Limit, Retry-After", + "type": "string" + } + }, + "Cache-Control": { + "schema": { + "const": "no-store", + "type": "string" + } + }, + "Pragma": { + "schema": { + "const": "no-cache", + "type": "string" + } + }, + "RateLimit-Limit": { + "schema": { + "const": "10", + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Static draft-11 named policy. No remaining or reset state is claimed.", + "schema": { + "const": "\"agent-batch\";q=10;w=60", + "type": "string" + } + }, + "X-Content-Type-Options": { + "schema": { + "const": "nosniff", + "type": "string" + } + }, + "X-RateLimit-Limit": { + "schema": { + "const": "10", + "type": "string" + } + } + } + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchRouteError" + } + } + }, + "description": "Trusted client identity is absent or the fail-closed batch limiter rejected the request.", + "headers": { + "Access-Control-Allow-Origin": { + "schema": { + "const": "*", + "type": "string" + } + }, + "Access-Control-Expose-Headers": { + "schema": { + "const": "RateLimit-Policy, RateLimit-Limit, X-RateLimit-Limit, Retry-After", + "type": "string" + } + }, + "Cache-Control": { + "schema": { + "const": "no-store", + "type": "string" + } + }, + "Pragma": { + "schema": { + "const": "no-cache", + "type": "string" + } + }, + "RateLimit-Limit": { + "schema": { + "const": "10", + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Static draft-11 named policy. No remaining or reset state is claimed.", + "schema": { + "const": "\"agent-batch\";q=10;w=60", + "type": "string" + } + }, + "Retry-After": { + "schema": { + "const": "60", + "type": "string" + } + }, + "X-Content-Type-Options": { + "schema": { + "const": "nosniff", + "type": "string" + } + }, + "X-RateLimit-Limit": { + "schema": { + "const": "10", + "type": "string" + } + } + } + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchRouteError" + } + } + }, + "description": "Batch execution or validated response serialization failed.", + "headers": { + "Access-Control-Allow-Origin": { + "schema": { + "const": "*", + "type": "string" + } + }, + "Access-Control-Expose-Headers": { + "schema": { + "const": "RateLimit-Policy, RateLimit-Limit, X-RateLimit-Limit, Retry-After", + "type": "string" + } + }, + "Cache-Control": { + "schema": { + "const": "no-store", + "type": "string" + } + }, + "Pragma": { + "schema": { + "const": "no-cache", + "type": "string" + } + }, + "RateLimit-Limit": { + "schema": { + "const": "10", + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Static draft-11 named policy. No remaining or reset state is claimed.", + "schema": { + "const": "\"agent-batch\";q=10;w=60", + "type": "string" + } + }, + "X-Content-Type-Options": { + "schema": { + "const": "nosniff", + "type": "string" + } + }, + "X-RateLimit-Limit": { + "schema": { + "const": "10", + "type": "string" + } + } + } + } + }, + "security": [], + "summary": "Execute a bounded batch of public reads", + "x-known-item-schema": { + "$ref": "#/components/schemas/BatchKnownItem" + } + } + }, + "/api/v1/content": { + "get": { + "operationId": "listPublicContent", + "parameters": [ + { + "in": "query", + "name": "q", + "schema": { + "maxLength": 200, + "type": "string" + } + }, + { + "in": "query", + "name": "type", + "schema": { + "enum": [ + "docs", + "blog", + "page" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "schema": { + "default": 20, + "maximum": 50, + "minimum": 1, + "type": "integer" + } + }, + { + "in": "query", + "name": "cursor", + "schema": { + "maxLength": 256, + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContentPage" + } + } + }, + "description": "A deterministic page of public content.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"public-content\";q=60;w=60" + ], + "type": "string" + } + }, + "RateLimit-Reset": { + "description": "Legacy compatibility delta-seconds field from an earlier RateLimit Internet-Draft generation. This route supplies an explicit conservative wait equal to the policy window; it is not a live countdown and is never draft-11 t.", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Error", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"public-content\";q=60;w=60" + ], + "type": "string" + } + }, + "RateLimit-Reset": { + "description": "Legacy compatibility delta-seconds field from an earlier RateLimit Internet-Draft generation. This route supplies an explicit conservative wait equal to the policy window; it is not a live countdown and is never draft-11 t.", + "schema": { + "type": "string" + } + } + } + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Error", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"public-content\";q=60;w=60" + ], + "type": "string" + } + }, + "RateLimit-Reset": { + "description": "Legacy compatibility delta-seconds field from an earlier RateLimit Internet-Draft generation. This route supplies an explicit conservative wait equal to the policy window; it is not a live countdown and is never draft-11 t.", + "schema": { + "type": "string" + } + }, + "Retry-After": { + "schema": { + "enum": [ + "60" + ], + "type": "string" + } + } + } + } + }, + "security": [], + "summary": "List and search public site content with cursor pagination" + } + }, + "/api/v1/content-search": { + "get": { + "deprecated": true, + "description": "Search blog posts, docs, and pages. Deprecated for new integrations: use /api/v1/content?q=... for deterministic cursor pagination.", + "operationId": "searchContent", + "parameters": [ + { + "description": "Search query.", + "in": "query", + "name": "q", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ContentSearchResult" + }, + "type": "array" + } + } + }, + "description": "Content search results." + } + }, + "security": [], + "summary": "Search site content" + } + }, + "/api/v1/globe": { + "get": { + "description": "Geographic point data for the member globe visualization. Cached (5 min).", + "operationId": "getGlobeData", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "countries": { + "type": "integer" + }, + "plottedOrgs": { + "type": "integer" + }, + "points": { + "items": { + "$ref": "#/components/schemas/GlobePoint" + }, + "type": "array" + }, + "totalOrgs": { + "type": "integer" + } + }, + "type": "object" + } + } + }, + "description": "Globe points and totals." + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Error" + } + }, + "security": [], + "summary": "Member globe data" + } + }, + "/api/v1/sandbox/simulate": { + "post": { + "description": "Deterministic no-write contract simulator with exactly these fixture scenarios: agent_auth_challenge, agent_auth_pending, agent_auth_success, batch_mixed_result. Responses use the same production Zod response schemas and serializers with fixed synthetic values. The route never reads member data, calls production services, signs credentials, or performs writes. Requests are capped at 32768 bytes, and the trusted-IP policy allows 30 requests per 60 seconds.", + "operationId": "simulateAgentContract", + "requestBody": { + "content": { + "application/json": { + "example": { + "scenario": "agent_auth_challenge" + }, + "schema": { + "$ref": "#/components/schemas/SandboxSimulationRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "examples": { + "agent_auth_challenge": { + "summary": "Synthetic service authorization challenge", + "value": { + "claim": { + "expires_in": 600, + "interval": 5, + "user_code": "000000", + "verification_uri": "https://agentcommunity.org/agent/authorize?claim_attempt_token=sandbox_claim_attempt_fixture_only" + }, + "claim_token": "sandbox_claim_token_fixture_only", + "claim_token_expires": "2099-01-01T00:00:00.000Z", + "claim_url": "https://agentcommunity.org/agent/identity/claim", + "post_claim_scopes": [ + "agent.account.read", + "agent.registrations.read" + ], + "registration_id": "00000000-0000-4000-8000-000000000501", + "registration_type": "service_auth" + } + }, + "agent_auth_success": { + "summary": "Synthetic approved agent authorization response", + "value": { + "access_token": "sandbox_access_token_fixture_only", + "assertion_expires": "2099-01-01T00:00:00.000Z", + "expires_in": 3600, + "identity_assertion": "sandbox_identity_assertion_fixture_only", + "scope": "agent.account.read agent.registrations.read", + "token_type": "Bearer" + } + }, + "batch_mixed_result": { + "summary": "Synthetic mixed public-read batch response", + "value": { + "items": [ + { + "id": "sandbox-content", + "operation": "content.list", + "result": { + "items": [ + { + "description": "Synthetic fixture; no production content or member data.", + "href": "/sandbox/fixtures/content", + "title": "[sandbox fixture] Published content", + "type": "page" + } + ], + "page": { + "has_more": false, + "limit": 1, + "next_cursor": null + } + }, + "status": "ok" + }, + { + "error": { + "code": "unknown_operation", + "message": "Unsupported operation" + }, + "id": "sandbox-unknown", + "operation": "sandbox.unknown", + "status": "error" + } + ] + } + } + }, + "schema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "claim": { + "additionalProperties": false, + "properties": { + "expires_in": { + "const": 600, + "type": "number" + }, + "interval": { + "const": 5, + "type": "number" + }, + "user_code": { + "pattern": "^\\d{6}$", + "type": "string" + }, + "verification_uri": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "user_code", + "expires_in", + "verification_uri", + "interval" + ], + "type": "object" + }, + "claim_token": { + "maxLength": 8192, + "minLength": 1, + "pattern": "^[\\u0021-\\u007e]+$", + "type": "string" + }, + "claim_token_expires": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "claim_url": { + "const": "https://agentcommunity.org/agent/identity/claim", + "type": "string" + }, + "post_claim_scopes": { + "items": { + "enum": [ + "agent.account.read", + "agent.registrations.read" + ], + "type": "string" + }, + "maxItems": 2, + "minItems": 1, + "type": "array" + }, + "registration_id": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$", + "type": "string" + }, + "registration_type": { + "const": "service_auth", + "type": "string" + } + }, + "required": [ + "registration_id", + "registration_type", + "claim_url", + "claim_token", + "claim_token_expires", + "post_claim_scopes", + "claim" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "access_token": { + "maxLength": 8192, + "minLength": 1, + "pattern": "^[\\u0021-\\u007e]+$", + "type": "string" + }, + "assertion_expires": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "expires_in": { + "exclusiveMinimum": 0, + "maximum": 3600, + "type": "integer" + }, + "identity_assertion": { + "maxLength": 8192, + "minLength": 1, + "type": "string" + }, + "scope": { + "minLength": 1, + "type": "string" + }, + "token_type": { + "const": "Bearer", + "type": "string" + } + }, + "required": [ + "access_token", + "token_type", + "expires_in", + "scope", + "identity_assertion", + "assertion_expires" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "items": { + "items": { + "anyOf": [ + { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "id": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "type": "string" + }, + "operation": { + "const": "content.list", + "type": "string" + }, + "result": { + "additionalProperties": false, + "properties": { + "items": { + "items": { + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "href": { + "pattern": "^\\/(?!\\/)", + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "docs", + "blog", + "page" + ], + "type": "string" + } + }, + "required": [ + "title", + "description", + "href", + "type" + ], + "type": "object" + }, + "maxItems": 50, + "type": "array" + }, + "page": { + "additionalProperties": false, + "properties": { + "has_more": { + "type": "boolean" + }, + "limit": { + "maximum": 50, + "minimum": 1, + "type": "integer" + }, + "next_cursor": { + "anyOf": [ + { + "maxLength": 256, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "limit", + "next_cursor", + "has_more" + ], + "type": "object" + } + }, + "required": [ + "items", + "page" + ], + "type": "object" + }, + "status": { + "const": "ok", + "type": "string" + } + }, + "required": [ + "id", + "operation", + "status", + "result" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "id": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "type": "string" + }, + "operation": { + "const": "docs.ask", + "type": "string" + }, + "result": { + "additionalProperties": false, + "properties": { + "answer": { + "maxLength": 1500, + "type": "string" + }, + "query": { + "maxLength": 500, + "type": "string" + }, + "sources": { + "items": { + "additionalProperties": false, + "properties": { + "description": { + "maxLength": 500, + "type": "string" + }, + "excerpt": { + "maxLength": 320, + "type": "string" + }, + "path": { + "pattern": "^\\/(?!\\/)", + "type": "string" + }, + "title": { + "maxLength": 200, + "type": "string" + }, + "url": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "title", + "description", + "path", + "url", + "excerpt" + ], + "type": "object" + }, + "maxItems": 10, + "type": "array" + } + }, + "required": [ + "query", + "answer", + "sources" + ], + "type": "object" + }, + "status": { + "const": "ok", + "type": "string" + } + }, + "required": [ + "id", + "operation", + "status", + "result" + ], + "type": "object" + } + ] + }, + { + "additionalProperties": false, + "properties": { + "error": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "code": { + "const": "unknown_operation", + "type": "string" + }, + "message": { + "const": "Unsupported operation", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "invalid_arguments", + "type": "string" + }, + "message": { + "const": "Invalid arguments", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "operation_failed", + "type": "string" + }, + "message": { + "const": "Operation failed", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "deadline_exceeded", + "type": "string" + }, + "message": { + "const": "Batch deadline exceeded", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "response_too_large", + "type": "string" + }, + "message": { + "const": "Operation response exceeds its limit", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "code": { + "const": "total_response_too_large", + "type": "string" + }, + "message": { + "const": "Batch response budget exceeded", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + } + ] + }, + "id": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "type": "string" + }, + "operation": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "status": { + "const": "error", + "type": "string" + } + }, + "required": [ + "id", + "operation", + "status", + "error" + ], + "type": "object" + } + ] + }, + "maxItems": 10, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "items" + ], + "type": "object" + } + ] + } + } + }, + "description": "Deterministic successful fixture response.", + "headers": { + "Cache-Control": { + "schema": { + "const": "no-store", + "type": "string" + } + }, + "Pragma": { + "schema": { + "const": "no-cache", + "type": "string" + } + }, + "RateLimit-Limit": { + "schema": { + "const": "30", + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Static draft-11 named policy. No remaining or reset state is claimed.", + "schema": { + "const": "\"agent-sandbox\";q=30;w=60", + "type": "string" + } + }, + "X-Content-Type-Options": { + "schema": { + "const": "nosniff", + "type": "string" + } + }, + "X-RateLimit-Limit": { + "schema": { + "const": "30", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "examples": { + "agent_auth_pending": { + "summary": "Synthetic authorization-pending OAuth response", + "value": { + "error": "authorization_pending", + "error_description": "Authorization is still pending" + } + }, + "invalid_request": { + "summary": "Strict request validation failure", + "value": { + "error": "invalid_request" + } + } + }, + "schema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "error": { + "enum": [ + "invalid_request", + "unsupported_grant_type", + "invalid_target", + "invalid_grant", + "authorization_pending", + "slow_down", + "access_denied", + "expired_token", + "invalid_claim_token", + "claim_expired", + "claimed_or_in_flight", + "rate_limited", + "temporarily_unavailable" + ], + "type": "string" + }, + "error_description": { + "type": "string" + } + }, + "required": [ + "error", + "error_description" + ], + "type": "object" + }, + { + "$ref": "#/components/schemas/SandboxRouteError" + } + ] + } + } + }, + "description": "The authorization-pending fixture response, malformed JSON, or an invalid strict request.", + "headers": { + "Cache-Control": { + "schema": { + "const": "no-store", + "type": "string" + } + }, + "Pragma": { + "schema": { + "const": "no-cache", + "type": "string" + } + }, + "RateLimit-Limit": { + "schema": { + "const": "30", + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Static draft-11 named policy. No remaining or reset state is claimed.", + "schema": { + "const": "\"agent-sandbox\";q=30;w=60", + "type": "string" + } + }, + "X-Content-Type-Options": { + "schema": { + "const": "nosniff", + "type": "string" + } + }, + "X-RateLimit-Limit": { + "schema": { + "const": "30", + "type": "string" + } + } + } + }, + "413": { + "content": { + "application/json": { + "example": { + "error": "request_too_large" + }, + "schema": { + "$ref": "#/components/schemas/SandboxRouteError" + } + } + }, + "description": "Request body exceeds 32768 bytes.", + "headers": { + "Cache-Control": { + "schema": { + "const": "no-store", + "type": "string" + } + }, + "Pragma": { + "schema": { + "const": "no-cache", + "type": "string" + } + }, + "RateLimit-Limit": { + "schema": { + "const": "30", + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Static draft-11 named policy. No remaining or reset state is claimed.", + "schema": { + "const": "\"agent-sandbox\";q=30;w=60", + "type": "string" + } + }, + "X-Content-Type-Options": { + "schema": { + "const": "nosniff", + "type": "string" + } + }, + "X-RateLimit-Limit": { + "schema": { + "const": "30", + "type": "string" + } + } + } + }, + "415": { + "content": { + "application/json": { + "example": { + "error": "unsupported_media_type" + }, + "schema": { + "$ref": "#/components/schemas/SandboxRouteError" + } + } + }, + "description": "Content-Type is not application/json with an optional UTF-8 charset.", + "headers": { + "Cache-Control": { + "schema": { + "const": "no-store", + "type": "string" + } + }, + "Pragma": { + "schema": { + "const": "no-cache", + "type": "string" + } + }, + "RateLimit-Limit": { + "schema": { + "const": "30", + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Static draft-11 named policy. No remaining or reset state is claimed.", + "schema": { + "const": "\"agent-sandbox\";q=30;w=60", + "type": "string" + } + }, + "X-Content-Type-Options": { + "schema": { + "const": "nosniff", + "type": "string" + } + }, + "X-RateLimit-Limit": { + "schema": { + "const": "30", + "type": "string" + } + } + } + }, + "429": { + "content": { + "application/json": { + "example": { + "error": "rate_limited" + }, + "schema": { + "$ref": "#/components/schemas/SandboxRouteError" + } + } + }, + "description": "Trusted client identity is absent or the fail-closed sandbox limiter rejected the request.", + "headers": { + "Cache-Control": { + "schema": { + "const": "no-store", + "type": "string" + } + }, + "Pragma": { + "schema": { + "const": "no-cache", + "type": "string" + } + }, + "RateLimit-Limit": { + "schema": { + "const": "30", + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Static draft-11 named policy. No remaining or reset state is claimed.", + "schema": { + "const": "\"agent-sandbox\";q=30;w=60", + "type": "string" + } + }, + "Retry-After": { + "schema": { + "const": "60", + "type": "string" + } + }, + "X-Content-Type-Options": { + "schema": { + "const": "nosniff", + "type": "string" + } + }, + "X-RateLimit-Limit": { + "schema": { + "const": "30", + "type": "string" + } + } + } + }, + "500": { + "content": { + "application/json": { + "example": { + "error": "simulation_unavailable" + }, + "schema": { + "$ref": "#/components/schemas/SandboxRouteError" + } + } + }, + "description": "Simulation response construction or validation failed.", + "headers": { + "Cache-Control": { + "schema": { + "const": "no-store", + "type": "string" + } + }, + "Pragma": { + "schema": { + "const": "no-cache", + "type": "string" + } + }, + "RateLimit-Limit": { + "schema": { + "const": "30", + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Static draft-11 named policy. No remaining or reset state is claimed.", + "schema": { + "const": "\"agent-sandbox\";q=30;w=60", + "type": "string" + } + }, + "X-Content-Type-Options": { + "schema": { + "const": "nosniff", + "type": "string" + } + }, + "X-RateLimit-Limit": { + "schema": { + "const": "30", + "type": "string" + } + } + } + } + }, + "security": [], + "summary": "Simulate an agent protocol response without writes" + } + }, + "/ask": { + "get": { + "description": "NLWeb v0.55-compatible deterministic extractive answers from published documentation only. The response includes Schema.org Article citations and never reads member, authenticated, Supabase, or private data. Set streaming=true or Accept: text/event-stream for start, result, complete SSE events.", + "operationId": "askPublicDocs", + "parameters": [ + { + "description": "Natural-language question. Omit every parameter to receive a capability envelope describing this endpoint instead of an answer.", + "in": "query", + "name": "query", + "required": false, + "schema": { + "maxLength": 500, + "minLength": 2, + "type": "string" + } + }, + { + "in": "query", + "name": "top_k", + "schema": { + "default": 5, + "maximum": 10, + "minimum": 1, + "type": "integer" + } + }, + { + "in": "query", + "name": "streaming", + "schema": { + "enum": [ + "true", + "false", + "1", + "0" + ], + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "example": { + "_meta": { + "mode": "list", + "response_type": "answer", + "site": "agentcommunity.org", + "version": "0.55" + }, + "answer": "AID: AID provides an identity and discovery record for agents.", + "content": [ + { + "@context": "https://schema.org", + "@id": "https://agentcommunity.org/docs/aid", + "@type": "Article", + "description": "AID documentation.", + "name": "AID", + "url": "https://agentcommunity.org/docs/aid" + } + ], + "query": "What is AID?" + }, + "schema": { + "$ref": "#/components/schemas/NLWebAnswer" + } + }, + "text/event-stream": { + "schema": { + "description": "SSE events named start, result, and complete only.", + "type": "string" + } + } + }, + "description": "NLWeb v0.55 answer or compatible event stream.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"docs-agent\";q=60;w=60" + ], + "type": "string" + } + }, + "RateLimit-Reset": { + "description": "Legacy compatibility delta-seconds field from an earlier RateLimit Internet-Draft generation. This route supplies an explicit conservative wait equal to the policy window; it is not a live countdown and is never draft-11 t.", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Error" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Error", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"docs-agent\";q=60;w=60" + ], + "type": "string" + } + }, + "RateLimit-Reset": { + "description": "Legacy compatibility delta-seconds field from an earlier RateLimit Internet-Draft generation. This route supplies an explicit conservative wait equal to the policy window; it is not a live countdown and is never draft-11 t.", + "schema": { + "type": "string" + } + }, + "Retry-After": { + "schema": { + "enum": [ + "60" + ], + "type": "string" + } + } + } + } + }, + "security": [], + "summary": "Answer questions from published documentation" + }, + "post": { + "description": "Accepts flat parameters or an NLWeb v0.55 request with query.text, optional query.site=agentcommunity.org, prefer.streaming, and optional meta.version=0.55. Responses are deterministic and public-documents-only.", + "operationId": "askPublicDocsStructured", + "requestBody": { + "content": { + "application/json": { + "examples": { + "flat": { + "value": { + "query": "What is AID?", + "streaming": false, + "top_k": 5 + } + }, + "structured": { + "value": { + "meta": { + "version": "0.55" + }, + "prefer": { + "streaming": false + }, + "query": { + "site": "agentcommunity.org", + "text": "What is AID?", + "top_k": 5 + } + } + } + }, + "schema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "query": { + "maxLength": 500, + "minLength": 2, + "type": "string" + }, + "streaming": { + "oneOf": [ + { + "type": "boolean" + }, + { + "enum": [ + "true", + "false", + "1", + "0" + ], + "type": "string" + } + ] + }, + "top_k": { + "default": 5, + "maximum": 10, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "meta": { + "additionalProperties": false, + "properties": { + "version": { + "const": "0.55", + "type": "string" + } + }, + "type": "object" + }, + "prefer": { + "additionalProperties": false, + "properties": { + "streaming": { + "type": "boolean" + } + }, + "type": "object" + }, + "query": { + "additionalProperties": false, + "properties": { + "site": { + "const": "agentcommunity.org", + "type": "string" + }, + "text": { + "maxLength": 500, + "minLength": 2, + "type": "string" + }, + "top_k": { + "default": 5, + "maximum": 10, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "text" + ], + "type": "object" + } + }, + "required": [ + "query" + ], + "type": "object" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "example": { + "_meta": { + "mode": "list", + "response_type": "answer", + "site": "agentcommunity.org", + "version": "0.55" + }, + "answer": "AID: AID provides an identity and discovery record for agents.", + "content": [ + { + "@context": "https://schema.org", + "@id": "https://agentcommunity.org/docs/aid", + "@type": "Article", + "description": "AID documentation.", + "name": "AID", + "url": "https://agentcommunity.org/docs/aid" + } + ], + "query": "What is AID?" + }, + "schema": { + "$ref": "#/components/schemas/NLWebAnswer" + } + }, + "text/event-stream": { + "schema": { + "description": "SSE events named start, result, and complete only.", + "type": "string" + } + } + }, + "description": "NLWeb v0.55 answer or compatible event stream.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"docs-agent\";q=60;w=60" + ], + "type": "string" + } + }, + "RateLimit-Reset": { + "description": "Legacy compatibility delta-seconds field from an earlier RateLimit Internet-Draft generation. This route supplies an explicit conservative wait equal to the policy window; it is not a live countdown and is never draft-11 t.", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Error" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Error" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Error" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Error", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"docs-agent\";q=60;w=60" + ], + "type": "string" + } + }, + "RateLimit-Reset": { + "description": "Legacy compatibility delta-seconds field from an earlier RateLimit Internet-Draft generation. This route supplies an explicit conservative wait equal to the policy window; it is not a live countdown and is never draft-11 t.", + "schema": { + "type": "string" + } + }, + "Retry-After": { + "schema": { + "enum": [ + "60" + ], + "type": "string" + } + } + } + } + }, + "security": [], + "summary": "Answer questions with an NLWeb v0.55 request" + } + }, + "/mcp": { + "post": { + "description": "Model Context Protocol endpoint (JSON-RPC 2.0 over HTTP POST, protocol 2026-07-28, with 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05 still served; stateless, no auth). Dual-era on one endpoint, selected per request: a modern 2026-07-28 request carries a params._meta block (io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities) and the standard headers below, and gets resultType, cache hints, io.modelcontextprotocol/serverInfo, and the modern error codes at their mandated HTTP statuses (-32020 header missing or mismatched and -32022 unsupported protocol revision at 400, -32601 unknown method at 404). A legacy request uses the initialize handshake, requires none of those headers, and is answered exactly as before with bare results at 200. The method initialize always selects legacy semantics; server/discover always selects modern. Tools: lookup_member, get_community_stats, register_agent, verify_certificate. Exact lookup returns only a minimal public projection; broad directory search is unsupported. The generic response policy is RateLimit-Policy: \"mcp\";q=30;w=60. Once register-specific enforcement is attempted, it is RateLimit-Policy: \"mcp-register\";q=2;w=60, even if KV is unavailable. A current RateLimit field and legacy RateLimit-Remaining are present only when the KV counter supplies a real remaining value; no t is claimed. Idempotency replays that bypass register-specific enforcement retain the generic policy. Manifest at /.well-known/mcp.", + "operationId": "mcpJsonRpc", + "parameters": [ + { + "description": "Optional idempotency key honored for register_agent tool calls: the same key from the same caller IP within 24h returns the original result instead of re-registering. Replayed responses carry Idempotency-Replayed: true.", + "in": "header", + "name": "Idempotency-Key", + "required": false, + "schema": { + "maxLength": 255, + "type": "string" + } + }, + { + "description": "Protocol revision. An unsupported explicit value is rejected with 400; the header may be omitted, in which case 2025-03-26 is assumed per the transport spec. REQUIRED on modern 2026-07-28 requests — absent there it is -32020 at 400, and it must agree with params._meta io.modelcontextprotocol/protocolVersion or it is -32020 HeaderMismatch. Never echoed back; it is attacker-controlled.", + "in": "header", + "name": "MCP-Protocol-Version", + "required": false, + "schema": { + "enum": [ + "2026-07-28", + "2025-11-25", + "2025-06-18", + "2025-03-26", + "2024-11-05" + ], + "type": "string" + } + }, + { + "description": "REQUIRED on modern 2026-07-28 requests, and must equal the JSON-RPC method in the body. Absent or disagreeing is -32020 at 400. Ignored entirely on legacy requests.", + "in": "header", + "name": "Mcp-Method", + "required": false, + "schema": { + "examples": [ + "tools/list", + "tools/call", + "server/discover" + ], + "type": "string" + } + }, + { + "description": "REQUIRED on modern 2026-07-28 name-bearing methods (tools/call, resources/read, prompts/get), and must equal params.name or params.uri. Absent or disagreeing is -32020 at 400. Ignored on every other method and on all legacy requests.", + "in": "header", + "name": "Mcp-Name", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Accepted at CORS preflight and then ignored, in both eras. This server is stateless: nothing reads it, and no response ever carries one. Sending it changes nothing.", + "in": "header", + "name": "Mcp-Session-Id", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "examples": [ + "initialize", + "tools/list", + "tools/call" + ], + "type": "string" + }, + "params": { + "type": "object" + } + }, + "required": [ + "jsonrpc", + "method" + ], + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "JSON-RPC response.", + "headers": { + "Idempotency-Replayed": { + "description": "Present and set to \"true\" only when the response is a replay of a stored register_agent result for the given Idempotency-Key + caller IP.", + "schema": { + "enum": [ + "true" + ], + "type": "string" + } + }, + "RateLimit": { + "description": "Current draft-11 RateLimit Structured Field, present only when the register KV counter provides a real remaining value. r is real remaining quota; this route has no independently justified t.", + "schema": { + "examples": [ + "\"mcp-register\";r=1", + "\"mcp-register\";r=0" + ], + "type": "string" + } + }, + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"mcp\";q=30;w=60", + "\"mcp-register\";q=2;w=60" + ], + "type": "string" + } + }, + "RateLimit-Remaining": { + "description": "Legacy compatibility field present only when the register KV counter provides a real remaining value for the 2/60 mcp-register policy. It is absent when remaining is unknown and is never fabricated.", + "schema": { + "type": "string" + } + } + } + }, + "202": { + "description": "MCP JSON-RPC notification acknowledgement. An accepted id-less notification returns HTTP 202 with an empty response body; it does not create an asynchronous REST operation, job resource, polling URL, or Location header.", + "x-mcp-notification-acknowledgement": true + }, + "400": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "Modern-era protocol fault, with a JSON-RPC error body. -32020 when a required standard header (MCP-Protocol-Version, Mcp-Method, Mcp-Name) is absent or disagrees with the body; -32022 when the declared protocol revision is not served modern-style, carrying data: { supported, requested }; -32602 when a required params._meta member is missing. Legacy requests cannot produce any of these — they are answered at 200." + }, + "403": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "Origin header present and structurally invalid — malformed, the literal \"null\", or a non-http(s) scheme. JSON-RPC error body, never an HTML page, and the offending value is never echoed back. An absent or well-formed http(s) Origin is accepted." + }, + "404": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "Modern-era unknown method (-32601), with a JSON-RPC error body and the usual CORS headers. Includes ping, which 2026-07-28 removed. Legacy unknown methods return -32601 at 200 instead." + } + }, + "security": [], + "summary": "MCP server (streamable HTTP)" + } + }, + "/oauth2/revoke": { + "post": { + "description": "RFC 7009-style idempotent single-token revocation. Unknown or already-revoked values return the same empty 200. This does not cancel a delegation.", + "operationId": "revokeAgentAccessToken", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/AgentAccessTokenRevocation" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Token processed idempotently; empty response body.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-auth-poll\";q=60;w=60" + ], + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Malformed form.", + "headers": {} + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Present Origin was not the exact allowed origin.", + "headers": {} + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Authorization runtime is disabled.", + "headers": {} + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Request body exceeds 8 KiB.", + "headers": {} + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Content-Type is not application/x-www-form-urlencoded with optional UTF-8 charset.", + "headers": {} + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Trusted-IP revoke limit rejected the request.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-auth-poll\";q=60;w=60" + ], + "type": "string" + } + } + } + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Authorization service is unavailable.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-auth-poll\";q=60;w=60" + ], + "type": "string" + } + } + } + } + }, + "security": [], + "summary": "Revoke one access token" + } + }, + "/oauth2/token": { + "post": { + "description": "Strict form endpoint, 8 KiB maximum. Successful claim polls mint a fresh access-token/assertion pair each time; JWT Bearer exchange returns a fresh access token only.", + "operationId": "exchangeAgentAuthorizationGrant", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ClaimTokenGrant" + }, + { + "$ref": "#/components/schemas/AssertionGrant" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApprovedClaimTokenResponse" + }, + { + "$ref": "#/components/schemas/AssertionExchangeResponse" + } + ] + } + } + }, + "description": "Fresh credentials for an approved claim or valid assertion.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-auth-poll\";q=60;w=60" + ], + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "OAuth error: invalid request/target/grant, pending, slow_down, denied, or expired.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-auth-poll\";q=60;w=60" + ], + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Present Origin was not the exact allowed origin.", + "headers": {} + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Authorization runtime is disabled.", + "headers": {} + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Request body exceeds 8 KiB.", + "headers": {} + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Content-Type is not application/x-www-form-urlencoded with optional UTF-8 charset.", + "headers": {} + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Trusted-IP token limit rejected the request.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-auth-poll\";q=60;w=60" + ], + "type": "string" + } + } + } + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAuthError" + } + } + }, + "description": "Authorization service is unavailable.", + "headers": { + "RateLimit-Limit": { + "description": "Legacy compatibility quota field from an earlier RateLimit Internet-Draft generation.", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Current draft-11 RateLimit-Policy Structured Field. Each item starts with a quoted policy identifier; q is quota and w is the static policy window in seconds.", + "schema": { + "examples": [ + "\"agent-auth-poll\";q=60;w=60" + ], + "type": "string" + } + } + } + } + }, + "security": [], + "summary": "Poll a claim or exchange a service identity assertion" + } + } + }, + "security": [], + "servers": [ + { + "url": "https://agentcommunity.org" + } + ] +} diff --git a/contracts/page/1.0.0/rest.json b/contracts/page/1.0.0/rest.json new file mode 100644 index 0000000..54da638 --- /dev/null +++ b/contracts/page/1.0.0/rest.json @@ -0,0 +1,116 @@ +{ + "fixtures": [ + { + "name": "content_list", + "request": { + "headers": { + "Accept": "application/json" + }, + "method": "GET", + "path": "/api/v1/content?type=docs&limit=1" + }, + "response": { + "body": { + "items": [ + { + "description": "Synthetic published-content fixture.", + "href": "/docs", + "title": "[fixture] Documentation index", + "type": "docs" + } + ], + "page": { + "has_more": true, + "limit": 1, + "next_cursor": "fixture_cursor_page_2" + } + }, + "http_status": 200 + } + }, + { + "name": "content_search", + "request": { + "headers": { + "Accept": "application/json" + }, + "method": "GET", + "path": "/api/v1/content?q=agent%20onboarding&type=docs&limit=5" + }, + "response": { + "body": { + "items": [ + { + "description": "Synthetic published-content search fixture.", + "href": "/docs/agent-onboarding", + "title": "[fixture] Agent onboarding", + "type": "docs" + } + ], + "page": { + "has_more": false, + "limit": 5, + "next_cursor": null + } + }, + "http_status": 200 + } + }, + { + "name": "docs_ask_non_streaming", + "request": { + "body": { + "query": "How can agents discover Agent Community resources?", + "streaming": false, + "top_k": 3 + }, + "headers": { + "Accept": "application/json", + "Content-Type": "application/json" + }, + "method": "POST", + "path": "/ask" + }, + "response": { + "body": { + "_meta": { + "mode": "list", + "response_type": "answer", + "site": "agentcommunity.org", + "version": "0.55" + }, + "answer": "[fixture] Use the published discovery and documentation surfaces.", + "content": [ + { + "@context": "https://schema.org", + "@id": "https://agentcommunity.org/docs/agent-onboarding", + "@type": "Article", + "description": "Synthetic published-document fixture.", + "name": "[fixture] Agent onboarding", + "url": "https://agentcommunity.org/docs/agent-onboarding" + } + ], + "query": "How can agents discover Agent Community resources?", + "results": [ + { + "description": "Synthetic published-document fixture.", + "name": "[fixture] Agent onboarding", + "schema_object": { + "@context": "https://schema.org", + "@id": "https://agentcommunity.org/docs/agent-onboarding", + "@type": "Article", + "description": "Synthetic published-document fixture.", + "name": "[fixture] Agent onboarding", + "url": "https://agentcommunity.org/docs/agent-onboarding" + }, + "site": "agentcommunity.org", + "url": "https://agentcommunity.org/docs/agent-onboarding" + } + ] + }, + "http_status": 200 + } + } + ], + "origin": "https://agentcommunity.org" +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..9657640 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,15 @@ +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist/**", "node_modules/**", "contracts/**"] }, + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["**/*.ts"], + rules: { + "@typescript-eslint/consistent-type-imports": "error", + "@typescript-eslint/no-explicit-any": "error", + }, + }, +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3d7f496 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4629 @@ +{ + "name": "@agentcommunity/cli", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@agentcommunity/cli", + "version": "0.1.0", + "license": "MIT", + "os": [ + "darwin", + "linux" + ], + "dependencies": { + "zod": "^4.0.14" + }, + "bin": { + "agentcommunity": "dist/cli.js" + }, + "devDependencies": { + "@eslint/js": "^9.32.0", + "@types/node": "^24.1.0", + "eslint": "^9.32.0", + "tsup": "^8.5.0", + "tsx": "^4.20.3", + "typescript": "^5.9.2", + "typescript-eslint": "^8.38.0", + "vitest": "^4.0.0" + }, + "engines": { + "node": "^22.14.0 || ^24.0.0 || ^26.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", + "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "2.0.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", + "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", + "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", + "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", + "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.1.tgz", + "integrity": "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.1.tgz", + "integrity": "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.1.tgz", + "integrity": "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.1.tgz", + "integrity": "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.1.tgz", + "integrity": "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.1.tgz", + "integrity": "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.1.tgz", + "integrity": "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.1.tgz", + "integrity": "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.1.tgz", + "integrity": "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.1.tgz", + "integrity": "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.1.tgz", + "integrity": "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.1.tgz", + "integrity": "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.1.tgz", + "integrity": "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "2.0.0-alpha.3", + "@emnapi/runtime": "2.0.0-alpha.3", + "@napi-rs/wasm-runtime": "^1.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.1.tgz", + "integrity": "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.1.tgz", + "integrity": "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.65.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "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" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "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" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "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 + } + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz", + "integrity": "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.142.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.1", + "@rolldown/binding-darwin-arm64": "1.2.1", + "@rolldown/binding-darwin-x64": "1.2.1", + "@rolldown/binding-freebsd-x64": "1.2.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", + "@rolldown/binding-linux-arm64-gnu": "1.2.1", + "@rolldown/binding-linux-arm64-musl": "1.2.1", + "@rolldown/binding-linux-ppc64-gnu": "1.2.1", + "@rolldown/binding-linux-s390x-gnu": "1.2.1", + "@rolldown/binding-linux-x64-gnu": "1.2.1", + "@rolldown/binding-linux-x64-musl": "1.2.1", + "@rolldown/binding-openharmony-arm64": "1.2.1", + "@rolldown/binding-wasm32-wasi": "1.2.1", + "@rolldown/binding-win32-arm64-msvc": "1.2.1", + "@rolldown/binding-win32-x64-msvc": "1.2.1" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "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 + } + } + }, + "node_modules/tsup/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "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" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", + "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.23", + "rolldown": "~1.2.0", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.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 + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@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.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "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 + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..730b617 --- /dev/null +++ b/package.json @@ -0,0 +1,57 @@ +{ + "name": "@agentcommunity/cli", + "version": "0.1.0", + "description": "Official read-only command-line client for Agent Community", + "type": "module", + "bin": { + "agentcommunity": "./dist/cli.js" + }, + "files": [ + "dist/", + "README.md", + "LICENSE", + "SECURITY.md" + ], + "scripts": { + "build": "tsup", + "contracts:check": "tsx scripts/check-contract-compat.ts", + "contracts:sync": "tsx scripts/sync-page-contracts.ts", + "lint": "eslint .", + "package:audit": "tsx scripts/audit-package.ts", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "engines": { + "node": "^22.14.0 || ^24.0.0 || ^26.0.0" + }, + "os": [ + "darwin", + "linux" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/agentcommunity/cli.git" + }, + "homepage": "https://agentcommunity.org/developers", + "bugs": { + "url": "https://github.com/agentcommunity/cli/issues" + }, + "license": "MIT", + "dependencies": { + "zod": "^4.0.14" + }, + "devDependencies": { + "@eslint/js": "^9.32.0", + "@types/node": "^24.1.0", + "eslint": "^9.32.0", + "tsx": "^4.20.3", + "tsup": "^8.5.0", + "typescript": "^5.9.2", + "typescript-eslint": "^8.38.0", + "vitest": "^4.0.0" + } +} diff --git a/scripts/audit-package.ts b/scripts/audit-package.ts new file mode 100644 index 0000000..14849bf --- /dev/null +++ b/scripts/audit-package.ts @@ -0,0 +1,66 @@ +import { createHash } from "node:crypto"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { basename, join } from "node:path"; +import { spawnSync } from "node:child_process"; + +interface PackFile { path: string; size: number } +interface PackResult { filename: string; files: Array } + +const expectedFiles = ["LICENSE", "README.md", "SECURITY.md", "dist/cli.js", "package.json"]; +const secretPatterns = [ + /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/, + /\bnpm_[A-Za-z0-9]{20,}\b/, + /\bgh[opusr]_[A-Za-z0-9]{20,}\b/, + /\bsk_(?:live|test)_[A-Za-z0-9]{16,}\b/, + /\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}\b/, +]; + +function command(commandName: string, args: Array, cwd: string): string { + const result = spawnSync(commandName, args, { cwd, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }); + if (result.status !== 0) throw new Error(`${commandName} ${args.join(" ")} failed:\n${result.stderr}`); + return result.stdout; +} + +async function main(): Promise { + const repositoryRoot = new URL("../", import.meta.url).pathname; + const packageJson = JSON.parse(await readFile(join(repositoryRoot, "package.json"), "utf8")); + if (packageJson.name !== "@agentcommunity/cli" || packageJson.bin?.agentcommunity !== "./dist/cli.js" || packageJson.exports !== undefined) { + throw new Error("Package metadata is outside the CLI-only boundary."); + } + if (packageJson.engines?.node !== "^22.14.0 || ^24.0.0 || ^26.0.0" || JSON.stringify(packageJson.os) !== JSON.stringify(["darwin", "linux"])) { + throw new Error("Node/OS package metadata drift detected."); + } + if (packageJson.homepage !== "https://agentcommunity.org/developers" || packageJson.repository?.url !== "git+https://github.com/agentcommunity/cli.git" || packageJson.bugs?.url !== "https://github.com/agentcommunity/cli/issues") { + throw new Error("Public package discoverability metadata drift detected."); + } + const destination = await mkdtemp(join(tmpdir(), "agentcommunity-pack-")); + const project = await mkdtemp(join(tmpdir(), "agentcommunity-install-")); + try { + const packed = JSON.parse(command("npm", ["pack", "--json", "--pack-destination", destination], repositoryRoot)) as Array; + const result = packed[0]; + if (result === undefined) throw new Error("npm pack returned no tarball."); + const inventory = result.files.map((file) => file.path).sort(); + if (JSON.stringify(inventory) !== JSON.stringify(expectedFiles)) throw new Error(`Unexpected package inventory: ${inventory.join(", ")}`); + const tarballPath = join(destination, basename(result.filename)); + const tarball = await readFile(tarballPath); + const tarballSha256 = createHash("sha256").update(tarball).digest("hex"); + const executable = await readFile(join(repositoryRoot, "dist/cli.js"), "utf8"); + if (!executable.startsWith("#!/usr/bin/env node\n")) throw new Error("Built binary is missing the Node shebang."); + for (const path of expectedFiles) { + const content = path === "dist/cli.js" ? executable : await readFile(join(repositoryRoot, path), "utf8"); + for (const pattern of secretPatterns) if (pattern.test(content)) throw new Error(`Possible secret in packed file ${path}.`); + } + await writeFile(join(project, "package.json"), '{"name":"agentcommunity-clean-install","private":true,"version":"1.0.0"}\n'); + command("npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund", tarballPath], project); + const smoke = spawnSync("npx", ["--no-install", "agentcommunity", "--help"], { cwd: project, encoding: "utf8", maxBuffer: 1024 * 1024 }); + const help = `${smoke.stdout}${smoke.stderr}`; + if (smoke.status !== 0 || !help.includes("Agent Community read-only CLI") || !help.includes("agentcommunity batch ")) throw new Error(`Clean-install binary help smoke failed. Output:\n${help}`); + process.stdout.write(`${JSON.stringify({ filename: result.filename, sha256: tarballSha256, files: result.files, clean_install_help: "passed" }, null, 2)}\n`); + } finally { + await rm(destination, { recursive: true, force: true }); + await rm(project, { recursive: true, force: true }); + } +} + +await main(); diff --git a/scripts/check-contract-compat.ts b/scripts/check-contract-compat.ts new file mode 100644 index 0000000..90047e8 --- /dev/null +++ b/scripts/check-contract-compat.ts @@ -0,0 +1,45 @@ +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { join } from "node:path"; + +import { verifyContractDirectory } from "../src/contracts.js"; +import { MCP_PROTOCOL_VERSION, PRODUCT_TOOL_NAMES } from "../src/mcp.js"; + +interface McpBundle { + advertised_revision: string; + supported_revisions: Array; + product_tools: Array<{ name: string }>; + fixtures: { + modern: { + tools_list: { response: { body: { result: { tools: Array<{ name: string }> } } } }; + tool_calls: Array<{ tool: string }>; + }; + }; +} + +async function main(): Promise { + const repositoryRoot = new URL("../", import.meta.url); + const verified = await verifyContractDirectory(repositoryRoot); + const rootPath = fileURLToPath(repositoryRoot); + const mcp = JSON.parse(await readFile(join(rootPath, "contracts/page/1.0.0/mcp.json"), "utf8")) as McpBundle; + const expectedTools = [...PRODUCT_TOOL_NAMES]; + if (mcp.advertised_revision !== MCP_PROTOCOL_VERSION || !mcp.supported_revisions.includes(MCP_PROTOCOL_VERSION)) { + throw new Error("The pinned bundle does not support the CLI modern MCP revision."); + } + if (JSON.stringify(mcp.product_tools.map((tool) => tool.name)) !== JSON.stringify(expectedTools)) { + throw new Error("Unclassified MCP product-tool drift detected."); + } + if (JSON.stringify(mcp.fixtures.modern.tools_list.response.body.result.tools.map((tool) => tool.name)) !== JSON.stringify(expectedTools)) { + throw new Error("Modern tools/list fixture drift detected."); + } + const expectedCalls = ["lookup_member", "get_community_stats", "verify_certificate"]; + if (JSON.stringify(mcp.fixtures.modern.tool_calls.map((call) => call.tool)) !== JSON.stringify(expectedCalls)) { + throw new Error("Read-only MCP call fixture drift detected."); + } + if (mcp.fixtures.modern.tool_calls.some((call) => call.tool === "register_agent")) { + throw new Error("The read-only CLI must not contain a register_agent call fixture."); + } + process.stdout.write(`Compatible PAGE bundle ${verified.bundleVersion} (${verified.manifestSha256}); exact product tools: ${expectedTools.join(", ")}.\n`); +} + +await main(); diff --git a/scripts/sync-page-contracts.ts b/scripts/sync-page-contracts.ts new file mode 100644 index 0000000..f6c6fe6 --- /dev/null +++ b/scripts/sync-page-contracts.ts @@ -0,0 +1,141 @@ +import { createHash } from "node:crypto"; +import { access, mkdtemp, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { z } from "zod"; + +export interface ContractLock { + bundle_version: "1.0.0"; + compatible_range: "^1.0.0"; + manifest_url: string; + manifest_sha256: string; +} + +const exactOrigin = "https://agentcommunity.org"; +const payloadNames = ["auth.json", "batch.json", "mcp.json", "openapi.json", "rest.json"] as const; +const lockSchema = z.object({ + bundle_version: z.literal("1.0.0"), compatible_range: z.literal("^1.0.0"), + manifest_url: z.literal("https://agentcommunity.org/.well-known/agentcommunity-contracts/1.0.0/manifest.json"), + manifest_sha256: z.string().regex(/^sha256:[0-9a-f]{64}$/), +}).strict(); +const manifestSchema = z.object({ + format_version: z.literal(1), bundle_version: z.literal("1.0.0"), page_source: z.literal("agentcommunity-page-openapi@1.3.0"), + openapi_version: z.literal("1.3.0"), workos_auth_md_commit: z.string().regex(/^[0-9a-f]{40}$/), contract_mode: z.literal("public"), + files: z.array(z.object({ + path: z.enum(payloadNames), media_type: z.literal("application/json"), bytes: z.number().int().nonnegative().max(2_097_152), + sha256: z.string().regex(/^sha256:[0-9a-f]{64}$/), + }).strict()).length(5), +}).strict(); + +function digest(bytes: Uint8Array): string { + return `sha256:${createHash("sha256").update(bytes).digest("hex")}`; +} + +function assertExactUrl(urlValue: string, expectedPath: string): URL { + const url = new URL(urlValue); + if (url.protocol !== "https:" || url.origin !== exactOrigin || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || url.pathname !== expectedPath || url.pathname.includes("..")) { + throw new Error("Contract URL is not an exact allowed Agent Community URL."); + } + return url; +} + +async function readBounded(response: Response, maximum: number): Promise { + const declared = response.headers.get("content-length"); + if (declared !== null && (!/^\d+$/.test(declared) || Number(declared) > maximum)) throw new Error("Contract response is too large."); + if (response.body === null) return new Uint8Array(); + const chunks: Array = []; + let total = 0; + const reader = response.body.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + total += value.byteLength; + if (total > maximum) { + await reader.cancel(); + throw new Error("Contract response is too large."); + } + chunks.push(value); + } + const bytes = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + return bytes; +} + +async function fetchExact(fetchImpl: typeof fetch, url: URL, maximum: number, json: boolean): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 10_000); + try { + const response = await fetchImpl(url, { headers: { Accept: json ? "application/json" : "text/plain" }, redirect: "manual", signal: controller.signal }); + if (response.status !== 200) throw new Error(`Contract fetch failed with HTTP ${response.status}.`); + const mime = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase(); + if (json ? mime !== "application/json" : mime !== "text/plain") throw new Error("Contract fetch returned an invalid content type."); + return await readBounded(response, maximum); + } finally { + clearTimeout(timer); + } +} + +export async function fetchContractBundle(rawLock: ContractLock, fetchImpl: typeof fetch = fetch): Promise> { + const lock = lockSchema.parse(rawLock); + const basePath = `/.well-known/agentcommunity-contracts/${lock.bundle_version}/`; + const manifestUrl = assertExactUrl(lock.manifest_url, `${basePath}manifest.json`); + const manifestBytes = await fetchExact(fetchImpl, manifestUrl, 65_536, true); + if (digest(manifestBytes) !== lock.manifest_sha256) throw new Error("Manifest hash does not match the lock."); + const manifest = manifestSchema.parse(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(manifestBytes))); + if (manifest.files.map((file) => file.path).join("\n") !== payloadNames.join("\n")) throw new Error("Manifest inventory is not exact and lexical."); + + const detachedUrl = assertExactUrl(new URL("manifest.sha256", manifestUrl).href, `${basePath}manifest.sha256`); + const detachedBytes = await fetchExact(fetchImpl, detachedUrl, 256, false); + if (new TextDecoder("utf-8", { fatal: true }).decode(detachedBytes) !== `${lock.manifest_sha256.slice(7)} manifest.json\n`) { + throw new Error("Detached manifest hash is invalid."); + } + + const bundle = new Map([["manifest.json", manifestBytes], ["manifest.sha256", detachedBytes]]); + for (const file of manifest.files) { + if (file.path.includes("/") || file.path.includes("..")) throw new Error("Manifest path escape rejected."); + const fileUrl = assertExactUrl(new URL(file.path, manifestUrl).href, `${basePath}${file.path}`); + const bytes = await fetchExact(fetchImpl, fileUrl, file.bytes, true); + if (bytes.byteLength !== file.bytes || digest(bytes) !== file.sha256) throw new Error(`Contract payload failed verification: ${file.path}`); + JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)); + bundle.set(file.path, bytes); + } + return bundle; +} + +async function pathExists(path: string): Promise { + try { await access(path); return true; } catch { return false; } +} + +async function main(): Promise { + const repositoryRoot = fileURLToPath(new URL("../", import.meta.url)); + const lock = lockSchema.parse(JSON.parse(await readFile(join(repositoryRoot, "contracts/page.lock.json"), "utf8"))) as ContractLock; + const bundle = await fetchContractBundle(lock); + const destination = join(repositoryRoot, "contracts/page", lock.bundle_version); + if (await pathExists(destination)) { + for (const [name, bytes] of bundle) { + const current = await readFile(join(destination, name)); + if (!current.equals(Buffer.from(bytes))) throw new Error(`Refusing to replace immutable bundle ${lock.bundle_version}.`); + } + process.stdout.write(`Contract bundle ${lock.bundle_version} already matches the verified remote bytes.\n`); + return; + } + await mkdir(dirname(destination), { recursive: true }); + const temporary = await mkdtemp(join(dirname(destination), `.sync-${lock.bundle_version}-`)); + try { + for (const [name, bytes] of bundle) await writeFile(join(temporary, name), bytes, { flag: "wx" }); + await rename(temporary, destination); + } catch (error) { + await rm(temporary, { recursive: true, force: true }); + throw error; + } + process.stdout.write(`Synced verified contract bundle ${lock.bundle_version}.\n`); +} + +const entry = process.argv[1]; +if (entry !== undefined && import.meta.url === pathToFileURL(entry).href) { + await main(); +} diff --git a/src/__tests__/commands.test.ts b/src/__tests__/commands.test.ts new file mode 100644 index 0000000..2a43a76 --- /dev/null +++ b/src/__tests__/commands.test.ts @@ -0,0 +1,166 @@ +import { describe, expect, test, vi } from "vitest"; + +import { runCli, type CliDependencies } from "../cli.js"; + +function harness(overrides: Partial = {}) { + let stdout = ""; + let stderr = ""; + const dependencies: CliDependencies = { + http: { requestJson: vi.fn() }, + mcp: { callTool: vi.fn() }, + readFile: vi.fn(), + readStdin: vi.fn(), + stdout: (value) => { stdout += value; }, + stderr: (value) => { stderr += value; }, + ...overrides, + }; + return { dependencies, output: () => ({ stdout, stderr }) }; +} + +describe("the seven read-only commands", () => { + test.each([ + ["stats", ["stats"], { member_count: 29_700, note: "Cached for up to 20 minutes." }], + ["member", ["member", "fixture-member"], { status: "member", matches: [{ display_name: "Fixture Member", member_since: null, profile_url: "https://agentcommunity.org/m/fixture-member" }] }], + ["verify", ["verify", "MESA-DD6-660J"], { certificate_id: "MESA-DD6-660J", status: "issued", valid_format: true, issued: true, agent_name: "mesa", certificate_url: "https://dmv.agentcommunity.org/certificates/MESA-DD6-660J" }], + ])("prints %s in JSON and human modes", async (_name, args, payload) => { + const jsonHarness = harness({ mcp: { callTool: vi.fn().mockResolvedValue(payload) } }); + expect(await runCli([...args, "--json"], jsonHarness.dependencies)).toBe(0); + expect(jsonHarness.output()).toEqual({ stdout: `${JSON.stringify(payload)}\n`, stderr: "" }); + + const humanHarness = harness({ mcp: { callTool: vi.fn().mockResolvedValue(payload) } }); + expect(await runCli(args, humanHarness.dependencies)).toBe(0); + expect(humanHarness.output().stdout.length).toBeGreaterThan(0); + expect(humanHarness.output().stderr).toBe(""); + }); + + test.each([ + ["content list", ["content", "list", "--type", "docs", "--limit", "1", "--cursor", "next"], "/api/v1/content?type=docs&limit=1&cursor=next", { items: [], page: { limit: 1, next_cursor: null, has_more: false } }], + ["content search", ["content", "search", "agent onboarding", "--type", "docs", "--limit", "5"], "/api/v1/content?q=agent+onboarding&type=docs&limit=5", { items: [], page: { limit: 5, next_cursor: null, has_more: false } }], + ["docs ask", ["docs", "ask", "What is AID?", "--top-k", "3"], "/ask", { query: "What is AID?", answer: "AID is a discovery format.", content: [], results: [], _meta: { mode: "list", response_type: "answer", site: "agentcommunity.org", version: "0.55" } }], + ])("maps %s directly in JSON and human modes", async (_name, args, expectedPath, payload) => { + const requestJson = vi.fn().mockResolvedValue(payload); + const jsonHarness = harness({ http: { requestJson } }); + expect(await runCli([...args, "--json"], jsonHarness.dependencies)).toBe(0); + expect(jsonHarness.output().stdout).toBe(`${JSON.stringify(payload)}\n`); + expect(requestJson).toHaveBeenCalledWith(expect.objectContaining({ path: expectedPath })); + if (expectedPath === "/ask") { + expect(requestJson).toHaveBeenCalledWith(expect.objectContaining({ body: { query: "What is AID?", top_k: 3, streaming: false } })); + } + + const humanHarness = harness({ http: { requestJson: vi.fn().mockResolvedValue(payload) } }); + expect(await runCli(args, humanHarness.dependencies)).toBe(0); + expect(humanHarness.output().stdout.length).toBeGreaterThan(0); + }); + + test("posts the validated batch unchanged, preserves order, and exits 8 for mixed results", async () => { + const request = { items: [ + { id: "first", operation: "content.list", arguments: {} }, + { id: "second", operation: "docs.ask", arguments: { query: "What is AID?" } }, + ] }; + const response = { items: [ + { id: "first", operation: "content.list", status: "ok", result: { items: [], page: { limit: 20, next_cursor: null, has_more: false } } }, + { id: "second", operation: "docs.ask", status: "error", error: { code: "operation_failed", message: "Operation failed" } }, + ] }; + const requestJson = vi.fn().mockResolvedValue(response); + const batchHarness = harness({ + http: { requestJson }, + readFile: vi.fn().mockResolvedValue(Buffer.from(JSON.stringify(request))), + }); + expect(await runCli(["batch", "batch.json", "--json"], batchHarness.dependencies)).toBe(8); + expect(batchHarness.output().stdout).toBe(`${JSON.stringify(response)}\n`); + expect(requestJson).toHaveBeenCalledWith(expect.objectContaining({ path: "/api/v1/batch", body: request })); + + const humanHarness = harness({ + http: { requestJson: vi.fn().mockResolvedValue(response) }, + readFile: vi.fn().mockResolvedValue(Buffer.from(JSON.stringify(request))), + }); + expect(await runCli(["batch", "batch.json"], humanHarness.dependencies)).toBe(8); + expect(humanHarness.output()).toEqual({ stdout: "first: ok\nsecond: error (operation_failed)\n", stderr: "" }); + }); + + test("reads batch input from stdin and rejects oversized input before JSON parsing", async () => { + const stdinHarness = harness({ + http: { requestJson: vi.fn().mockResolvedValue({ items: [{ id: "one", operation: "content.list", status: "ok", result: { items: [], page: { limit: 20, next_cursor: null, has_more: false } } }] }) }, + readStdin: vi.fn().mockResolvedValue(Buffer.from('{"items":[{"id":"one","operation":"content.list","arguments":{}}]}')), + }); + expect(await runCli(["batch", "-", "--json"], stdinHarness.dependencies)).toBe(0); + + const oversized = harness({ readFile: vi.fn().mockResolvedValue(Buffer.alloc(262_145, 0x7b)) }); + expect(await runCli(["batch", "huge.json"], oversized.dependencies)).toBe(2); + expect(oversized.dependencies.http.requestJson).not.toHaveBeenCalled(); + expect(oversized.output().stdout).toBe(""); + expect(JSON.parse(oversized.output().stderr)).toMatchObject({ error: { code: "input_too_large" } }); + }); + + test("maps an unreadable batch file to a local input error", async () => { + const batchHarness = harness({ readFile: vi.fn().mockRejectedValue(new Error("ENOENT /private/path")) }); + expect(await runCli(["batch", "missing.json"], batchHarness.dependencies)).toBe(2); + expect(batchHarness.output().stdout).toBe(""); + expect(JSON.parse(batchHarness.output().stderr)).toEqual({ error: { code: "file_read_error", message: "Batch input could not be read." } }); + expect(batchHarness.output().stderr).not.toContain("/private/path"); + }); + + test.each([ + [{ items: [{ id: "same", operation: "content.list", arguments: {} }, { id: "same", operation: "content.list", arguments: {} }] }, "duplicate_batch_id"], + [{ items: [{ id: "x", operation: "member.lookup", arguments: {} }] }, "member_operation_forbidden"], + ])("rejects malformed or member-capable batch envelopes locally", async (input, code) => { + const batchHarness = harness({ readFile: vi.fn().mockResolvedValue(Buffer.from(JSON.stringify(input))) }); + expect(await runCli(["batch", "input.json"], batchHarness.dependencies)).toBe(2); + expect(JSON.parse(batchHarness.output().stderr)).toMatchObject({ error: { code } }); + expect(batchHarness.dependencies.http.requestJson).not.toHaveBeenCalled(); + }); + + test("posts an unknown non-member operation unchanged for the contract-defined item error", async () => { + const request = { items: [{ id: "x", operation: "future.read", arguments: { fixture: true } }] }; + const response = { items: [{ id: "x", operation: "future.read", status: "error", error: { code: "unknown_operation", message: "Unsupported operation" } }] }; + const requestJson = vi.fn().mockResolvedValue(response); + const batchHarness = harness({ http: { requestJson }, readFile: vi.fn().mockResolvedValue(Buffer.from(JSON.stringify(request))) }); + expect(await runCli(["batch", "input.json", "--json"], batchHarness.dependencies)).toBe(8); + expect(requestJson).toHaveBeenCalledWith(expect.objectContaining({ body: request })); + }); + + test("maps negative member and certificate states to stable exits while printing payloads", async () => { + for (const [args, payload, exitCode] of [ + [["member", "missing", "--json"], { status: "not_found", matches: [] }, 3], + [["member", "shared", "--json"], { status: "ambiguous", matches: [] }, 3], + [["verify", "bad", "--json"], { certificate_id: "bad", status: "invalid_format", valid_format: false, issued: false, agent_name: null, certificate_url: null }, 2], + [["verify", "missing", "--json"], { certificate_id: "missing", status: "not_found", valid_format: true, issued: false, agent_name: null, certificate_url: null }, 3], + [["verify", "offline", "--json"], { certificate_id: "offline", status: "unavailable", valid_format: true, issued: null, agent_name: null, certificate_url: null }, 6], + ] as const) { + const resultHarness = harness({ mcp: { callTool: vi.fn().mockResolvedValue(payload) } }); + expect(await runCli([...args], resultHarness.dependencies)).toBe(exitCode); + expect(resultHarness.output()).toEqual({ stdout: `${JSON.stringify(payload)}\n`, stderr: "" }); + } + }); + + test("rejects invalid local options without making a request", async () => { + for (const args of [ + ["member", ""], ["member", "x".repeat(201)], ["content", "list", "--limit", "0"], + ["content", "list", "--type", "member"], ["docs", "ask", "x"], ["docs", "ask", "valid query", "--top-k", "11"], + ["stats", "--timeout", "999"], ["stats", "--timeout", "30001"], + ]) { + const resultHarness = harness(); + expect(await runCli(args, resultHarness.dependencies)).toBe(2); + expect(resultHarness.output().stdout).toBe(""); + expect(resultHarness.dependencies.http.requestJson).not.toHaveBeenCalled(); + expect(resultHarness.dependencies.mcp.callTool).not.toHaveBeenCalled(); + } + }); + + test("help and local usage have no hidden network or telemetry request", async () => { + const helpHarness = harness(); + expect(await runCli(["--help"], helpHarness.dependencies)).toBe(0); + expect(helpHarness.output().stdout).toContain("Agent Community read-only CLI"); + expect(helpHarness.dependencies.http.requestJson).not.toHaveBeenCalled(); + expect(helpHarness.dependencies.mcp.callTool).not.toHaveBeenCalled(); + }); + + test("emits one error envelope to stderr and nothing to stdout", async () => { + const errorHarness = harness({ mcp: { callTool: vi.fn().mockRejectedValue(new Error("secret request body")) } }); + expect(await runCli(["stats", "--json"], errorHarness.dependencies)).toBe(6); + expect(errorHarness.output().stdout).toBe(""); + const envelope = JSON.parse(errorHarness.output().stderr); + expect(envelope).toEqual({ error: { code: "network_error", message: "The Agent Community service could not be reached." } }); + expect(errorHarness.output().stderr).not.toContain("secret request body"); + }); +}); diff --git a/src/__tests__/contracts.test.ts b/src/__tests__/contracts.test.ts new file mode 100644 index 0000000..e251465 --- /dev/null +++ b/src/__tests__/contracts.test.ts @@ -0,0 +1,29 @@ +import { cp, mkdtemp, readFile, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, test } from "vitest"; + +import { verifyContractDirectory } from "../contracts.js"; + +const repositoryRoot = new URL("../../", import.meta.url); + +describe("vendored PAGE contracts", () => { + test("verifies the exact locked manifest and every vendored byte", async () => { + const result = await verifyContractDirectory(repositoryRoot); + expect(result).toEqual({ bundleVersion: "1.0.0", manifestSha256: "sha256:b1f10b6288e436ccdca282b88a9a9115fcc0f6716f90731aab1455175b535595" }); + }); + + test("refuses tampering and path escape inventory", async () => { + const temp = await mkdtemp(join(tmpdir(), "agentcommunity-contracts-")); + await cp(new URL("../../contracts", import.meta.url), join(temp, "contracts"), { recursive: true }); + const lockPath = join(temp, "contracts/page.lock.json"); + const lock = JSON.parse(await readFile(lockPath, "utf8")); + await writeFile(join(temp, "contracts/page/1.0.0/auth.json"), "{}\n"); + await expect(verifyContractDirectory(new URL(`file://${temp}/`))).rejects.toMatchObject({ code: "contract_mismatch" }); + + await cp(new URL("../../contracts/page/1.0.0/auth.json", import.meta.url), join(temp, "contracts/page/1.0.0/auth.json")); + lock.manifest_url = "https://agentcommunity.org/.well-known/agentcommunity-contracts/1.0.0/../escape.json"; + await writeFile(lockPath, JSON.stringify(lock)); + await expect(verifyContractDirectory(new URL(`file://${temp}/`))).rejects.toMatchObject({ code: "contract_mismatch" }); + }); +}); diff --git a/src/__tests__/http.test.ts b/src/__tests__/http.test.ts new file mode 100644 index 0000000..ea718e3 --- /dev/null +++ b/src/__tests__/http.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test, vi } from "vitest"; + +import { HttpClient } from "../http.js"; + +function response(body: string, init: ResponseInit = {}) { + return new Response(body, { status: 200, headers: { "content-type": "application/json", ...init.headers }, ...init }); +} + +describe("bounded HTTP transport", () => { + test("uses the fixed production origin, manual redirects, JSON headers, and no extra request", async () => { + const fetchImpl = vi.fn().mockResolvedValue(response('{"ok":true}')); + const client = new HttpClient(fetchImpl); + expect(await client.requestJson({ method: "GET", path: "/api/v1/content", timeoutMs: 1_000, maxBytes: 100, validate: (value) => value })).toEqual({ ok: true }); + expect(fetchImpl).toHaveBeenCalledTimes(1); + expect(fetchImpl).toHaveBeenCalledWith("https://agentcommunity.org/api/v1/content", expect.objectContaining({ redirect: "manual", method: "GET", headers: expect.objectContaining({ Accept: "application/json" }) })); + }); + + test.each([ + [response("", { status: 302, headers: { location: "https://evil.example" } }), 5, "redirect_rejected"], + [response("plain", { headers: { "content-type": "text/plain" } }), 5, "invalid_content_type"], + [response("not-json"), 5, "invalid_json"], + [response('{"wrong":true}'), 5, "schema_mismatch"], + [response('{"error":true}', { status: 503 }), 6, "upstream_unavailable"], + ])("maps protocol and upstream failures", async (remoteResponse, exitCode, code) => { + const client = new HttpClient(vi.fn().mockResolvedValue(remoteResponse)); + await expect(client.requestJson({ method: "GET", path: "/x", timeoutMs: 1_000, maxBytes: 100, validate: (value) => { + if (typeof value !== "object" || value === null || !("ok" in value)) throw new Error("schema"); + return value; + } })).rejects.toMatchObject({ exitCode, code }); + }); + + test("rejects a response over the byte cap", async () => { + const client = new HttpClient(vi.fn().mockResolvedValue(response(JSON.stringify({ value: "too long" })))); + await expect(client.requestJson({ method: "GET", path: "/x", timeoutMs: 1_000, maxBytes: 4, validate: (value) => value })).rejects.toMatchObject({ exitCode: 5, code: "response_too_large" }); + }); + + test("maps timeout and 429 with only bounded valid Retry-After information", async () => { + const timeoutClient = new HttpClient(vi.fn().mockRejectedValue(new DOMException("aborted", "AbortError"))); + await expect(timeoutClient.requestJson({ method: "GET", path: "/x", timeoutMs: 1_000, maxBytes: 10, validate: (value) => value })).rejects.toMatchObject({ exitCode: 6, code: "timeout" }); + + const rateClient = new HttpClient(vi.fn().mockResolvedValue(response('{"error":true}', { status: 429, headers: { "content-type": "application/json", "retry-after": "60" } }))); + await expect(rateClient.requestJson({ method: "GET", path: "/x", timeoutMs: 1_000, maxBytes: 100, validate: (value) => value })).rejects.toMatchObject({ exitCode: 7, code: "rate_limited", details: { retry_after_ms: 60_000 } }); + + const invalidClient = new HttpClient(vi.fn().mockResolvedValue(response('{"error":true}', { status: 429, headers: { "content-type": "application/json", "retry-after": "999999999" } }))); + await expect(invalidClient.requestJson({ method: "GET", path: "/x", timeoutMs: 1_000, maxBytes: 100, validate: (value) => value })).rejects.toMatchObject({ exitCode: 7, code: "rate_limited", details: undefined }); + }); +}); diff --git a/src/__tests__/mcp.test.ts b/src/__tests__/mcp.test.ts new file mode 100644 index 0000000..3edf1cc --- /dev/null +++ b/src/__tests__/mcp.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, test, vi } from "vitest"; + +import { McpClient, MCP_PROTOCOL_VERSION, PRODUCT_TOOL_NAMES } from "../mcp.js"; + +describe("modern MCP transport", () => { + test("sends the exact modern body and headers without tools/list", async () => { + const requestJson = vi.fn().mockResolvedValue({ + jsonrpc: "2.0", id: "test-id", result: { + resultType: "complete", content: [{ type: "text", text: '{"member_count":1,"note":"fixture"}' }], + structuredContent: { member_count: 1, note: "fixture" }, _meta: { "io.modelcontextprotocol/serverInfo": { name: "agentcommunity" } }, + }, + }); + const client = new McpClient({ requestJson }, () => "test-id", "0.1.0"); + await client.callTool("get_community_stats", {}, (value) => value); + expect(requestJson).toHaveBeenCalledTimes(1); + expect(requestJson).toHaveBeenCalledWith({ + method: "POST", path: "/mcp", timeoutMs: 10_000, maxBytes: 262_144, + headers: { "MCP-Protocol-Version": "2026-07-28", "Mcp-Method": "tools/call", "Mcp-Name": "get_community_stats" }, + body: { jsonrpc: "2.0", id: "test-id", method: "tools/call", params: { + _meta: { + "io.modelcontextprotocol/clientCapabilities": {}, + "io.modelcontextprotocol/clientInfo": { name: "@agentcommunity/cli", version: "0.1.0" }, + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + }, + arguments: {}, name: "get_community_stats", + } }, + validate: expect.any(Function), + }); + }); + + test("pins the modern revision and exact four-tool contract boundary", () => { + expect(MCP_PROTOCOL_VERSION).toBe("2026-07-28"); + expect(PRODUCT_TOOL_NAMES).toEqual(["lookup_member", "get_community_stats", "register_agent", "verify_certificate"]); + }); + + test("rejects register_agent and malformed or JSON-RPC error responses", async () => { + const requestJson = vi.fn(); + const client = new McpClient({ requestJson }, () => "test-id", "0.1.0"); + await expect(client.callTool("register_agent" as never, {}, (value) => value)).rejects.toMatchObject({ exitCode: 2 }); + expect(requestJson).not.toHaveBeenCalled(); + + requestJson.mockResolvedValueOnce({ jsonrpc: "2.0", id: "test-id", error: { code: -32602, message: "Invalid params" } }); + await expect(client.callTool("get_community_stats", {}, (value) => value)).rejects.toMatchObject({ exitCode: 5 }); + + requestJson.mockResolvedValueOnce({ + jsonrpc: "2.0", id: "test-id", result: { + resultType: "complete", _meta: {}, structuredContent: { member_count: 1, note: "different" }, + content: [{ type: "text", text: '{"member_count":2,"note":"mismatch"}' }], + }, + }); + await expect(client.callTool("get_community_stats", {}, (value) => value)).rejects.toMatchObject({ exitCode: 5 }); + }); +}); diff --git a/src/__tests__/package-boundary.test.ts b/src/__tests__/package-boundary.test.ts new file mode 100644 index 0000000..3cb77a0 --- /dev/null +++ b/src/__tests__/package-boundary.test.ts @@ -0,0 +1,32 @@ +import { readFile } from "node:fs/promises"; +import { describe, expect, test } from "vitest"; + +const root = new URL("../../", import.meta.url); + +describe("package and CI boundaries", () => { + test("declares a CLI-only package for the maintained Node and OS matrix", async () => { + const packageJson = JSON.parse(await readFile(new URL("package.json", root), "utf8")); + expect(packageJson).toMatchObject({ + name: "@agentcommunity/cli", type: "module", bin: { agentcommunity: "./dist/cli.js" }, + files: ["dist/", "README.md", "LICENSE", "SECURITY.md"], + engines: { node: "^22.14.0 || ^24.0.0 || ^26.0.0" }, os: ["darwin", "linux"], + publishConfig: { access: "public" }, + homepage: "https://agentcommunity.org/developers", + bugs: { url: "https://github.com/agentcommunity/cli/issues" }, + }); + expect(packageJson.exports).toBeUndefined(); + expect(packageJson.scripts.preinstall).toBeUndefined(); + expect(packageJson.scripts.postinstall).toBeUndefined(); + }); + + test("CI source contains all six required jobs and no publish permission or release workflow", async () => { + const workflow = await readFile(new URL(".github/workflows/ci.yml", root), "utf8"); + expect(workflow).toContain("ubuntu-24.04"); + expect(workflow).toContain("macos-14"); + expect(workflow).toContain("22.14.0"); + expect(workflow).toContain('"24"'); + expect(workflow).toContain('"26"'); + expect(workflow).not.toContain("id-token: write"); + await expect(readFile(new URL(".github/workflows/release.yml", root), "utf8")).rejects.toThrow(); + }); +}); diff --git a/src/__tests__/sync-page-contracts.test.ts b/src/__tests__/sync-page-contracts.test.ts new file mode 100644 index 0000000..8589003 --- /dev/null +++ b/src/__tests__/sync-page-contracts.test.ts @@ -0,0 +1,61 @@ +import { createHash } from "node:crypto"; +import { describe, expect, test, vi } from "vitest"; + +import { fetchContractBundle, type ContractLock } from "../../scripts/sync-page-contracts.js"; + +const origin = "https://agentcommunity.org/.well-known/agentcommunity-contracts/1.0.0/"; + +function hash(bytes: Uint8Array): string { + return `sha256:${createHash("sha256").update(bytes).digest("hex")}`; +} + +function fixture() { + const payloads = new Map([ + ["auth.json", Buffer.from("{}\n")], ["batch.json", Buffer.from("{}\n")], ["mcp.json", Buffer.from("{}\n")], + ["openapi.json", Buffer.from("{}\n")], ["rest.json", Buffer.from("{}\n")], + ]); + const manifest = Buffer.from(`${JSON.stringify({ + format_version: 1, bundle_version: "1.0.0", page_source: "agentcommunity-page-openapi@1.3.0", openapi_version: "1.3.0", + workos_auth_md_commit: "b53c9edfbfeea679b617727ebca9ba436bade794", contract_mode: "public", + files: [...payloads].map(([path, bytes]) => ({ path, media_type: "application/json", bytes: bytes.byteLength, sha256: hash(bytes) })), + }, null, 2)}\n`); + const manifestHash = hash(manifest); + const detached = Buffer.from(`${manifestHash.slice(7)} manifest.json\n`); + const lock: ContractLock = { + bundle_version: "1.0.0", compatible_range: "^1.0.0", manifest_url: `${origin}manifest.json`, manifest_sha256: manifestHash, + }; + return { payloads, manifest, detached, lock }; +} + +describe("contract sync download", () => { + test("fetches only exact sibling URLs with manual redirects and verifies every byte before returning", async () => { + const data = fixture(); + const bodies = new Map([["manifest.json", data.manifest], ["manifest.sha256", data.detached], ...data.payloads]); + const fetchImpl = vi.fn(async (url: string | URL | Request, init?: RequestInit) => { + expect(init?.redirect).toBe("manual"); + const name = String(url).slice(origin.length); + const bytes = bodies.get(name); + if (bytes === undefined) return new Response("missing", { status: 404 }); + return new Response(Buffer.from(bytes), { status: 200, headers: { "content-type": name.endsWith(".json") ? "application/json" : "text/plain" } }); + }); + const result = await fetchContractBundle(data.lock, fetchImpl); + expect([...result.keys()]).toEqual(["manifest.json", "manifest.sha256", "auth.json", "batch.json", "mcp.json", "openapi.json", "rest.json"]); + expect(fetchImpl).toHaveBeenCalledTimes(7); + }); + + test("fails closed on redirects, tampering, unexpected inventory, and unsafe URLs", async () => { + const data = fixture(); + await expect(fetchContractBundle({ ...data.lock, manifest_url: "https://evil.example/manifest.json" }, vi.fn())).rejects.toThrow(); + await expect(fetchContractBundle({ ...data.lock, manifest_url: `${origin}../manifest.json` }, vi.fn())).rejects.toThrow(); + + const redirectFetch = vi.fn().mockResolvedValue(new Response("", { status: 302, headers: { location: "https://evil.example" } })); + await expect(fetchContractBundle(data.lock, redirectFetch)).rejects.toThrow(); + + const tamperedFetch = vi.fn(async (url: string | URL | Request) => { + const name = String(url).slice(origin.length); + const bytes = name === "manifest.json" ? Buffer.from(data.manifest.toString().replace("1.0.0", "1.0.1")) : data.detached; + return new Response(Buffer.from(bytes), { status: 200, headers: { "content-type": name.endsWith(".json") ? "application/json" : "text/plain" } }); + }); + await expect(fetchContractBundle(data.lock, tamperedFetch)).rejects.toThrow(); + }); +}); diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..04c007e --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,220 @@ +import { randomUUID } from "node:crypto"; +import { open, realpath } from "node:fs/promises"; +import { pathToFileURL } from "node:url"; + +import { runBatch } from "./commands/batch.js"; +import { runContent, type ContentOptions } from "./commands/content.js"; +import { runDocsAsk } from "./commands/docs.js"; +import { runMember } from "./commands/member.js"; +import { runStats } from "./commands/stats.js"; +import { runVerify } from "./commands/verify.js"; +import { BATCH_INPUT_MAX_BYTES, parseTimeout } from "./config.js"; +import { CliError, type ExitCode, usageError } from "./errors.js"; +import { HttpClient, type HttpTransport } from "./http.js"; +import { McpClient, type McpTransport } from "./mcp.js"; + +export const CLI_VERSION = "0.1.0"; + +export interface CliDependencies { + http: HttpTransport; + mcp: McpTransport; + readFile(path: string, maxBytes?: number): Promise; + readStdin(maxBytes?: number): Promise; + stdout(value: string): void; + stderr(value: string): void; +} + +interface GlobalOptions { + json: boolean; + timeoutMs: number; + args: Array; +} + +interface CommandResult { + payload: unknown; + human: string; + exitCode: ExitCode; +} + +const HELP = `Agent Community read-only CLI + +Usage: + agentcommunity stats [--json] [--timeout ] + agentcommunity member [--json] [--timeout ] + agentcommunity verify [--json] [--timeout ] + agentcommunity content list [--type docs|blog|page] [--limit 1..50] [--cursor opaque] [--json] [--timeout ] + agentcommunity content search [--type docs|blog|page] [--limit 1..50] [--cursor opaque] [--json] [--timeout ] + agentcommunity docs ask [--top-k 1..10] [--json] [--timeout ] + agentcommunity batch [--json] [--timeout ] + +Exit codes: 0 success, 2 usage/input, 3 not found/ambiguous/not issued, +4 reserved for auth, 5 protocol/contract, 6 timeout/unavailable, +7 rate limited, 8 mixed batch result. +`; + +function parseGlobals(argv: Array): GlobalOptions { + let json = false; + let timeoutValue: string | undefined; + const args: Array = []; + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + if (argument === "--json") { + if (json) throw usageError("duplicate_option", "--json may be specified only once."); + json = true; + } else if (argument === "--timeout") { + if (timeoutValue !== undefined) throw usageError("duplicate_option", "--timeout may be specified only once."); + timeoutValue = argv[index + 1]; + if (timeoutValue === undefined) throw usageError("invalid_timeout", "--timeout requires a value."); + index += 1; + } else if (argument !== undefined) { + args.push(argument); + } + } + return { json, timeoutMs: parseTimeout(timeoutValue), args }; +} + +function parseNamedOptions(args: Array, allowed: ReadonlySet): { positional: Array; options: Record } { + const positional: Array = []; + const options: Record = {}; + for (let index = 0; index < args.length; index += 1) { + const argument = args[index]; + if (argument?.startsWith("--")) { + if (!allowed.has(argument)) throw usageError("unknown_option", `Unknown option: ${argument}`); + if (options[argument] !== undefined) throw usageError("duplicate_option", `${argument} may be specified only once.`); + const value = args[index + 1]; + if (value === undefined || value.startsWith("--")) throw usageError("missing_option_value", `${argument} requires a value.`); + options[argument] = value; + index += 1; + } else if (argument !== undefined) { + positional.push(argument); + } + } + return { positional, options }; +} + +function exactly(args: Array, count: number, usage: string): void { + if (args.length !== count) throw usageError("invalid_usage", usage); +} + +async function dispatch(options: GlobalOptions, dependencies: CliDependencies): Promise { + const [command, ...rest] = options.args; + if (command === undefined || command === "--help" || command === "-h" || command === "help") { + dependencies.stdout(HELP); + return null; + } + if (command === "--version" || command === "-v") { + exactly(rest, 0, "--version takes no arguments."); + dependencies.stdout(`${CLI_VERSION}\n`); + return null; + } + if (command === "stats") { + exactly(rest, 0, "Usage: agentcommunity stats"); + return runStats(dependencies.mcp, options.timeoutMs); + } + if (command === "member") { + exactly(rest, 1, "Usage: agentcommunity member "); + return runMember(dependencies.mcp, rest[0] ?? "", options.timeoutMs); + } + if (command === "verify") { + exactly(rest, 1, "Usage: agentcommunity verify "); + return runVerify(dependencies.mcp, rest[0] ?? "", options.timeoutMs); + } + if (command === "content") { + const [subcommand, ...contentArgs] = rest; + if (subcommand !== "list" && subcommand !== "search") throw usageError("invalid_usage", "Usage: agentcommunity content ..."); + const parsed = parseNamedOptions(contentArgs, new Set(["--type", "--limit", "--cursor"])); + exactly(parsed.positional, subcommand === "list" ? 0 : 1, subcommand === "list" ? "Usage: agentcommunity content list [options]" : "Usage: agentcommunity content search [options]"); + const contentOptions: ContentOptions = {}; + if (parsed.options["--type"] !== undefined) contentOptions.type = parsed.options["--type"]; + if (parsed.options["--limit"] !== undefined) contentOptions.limit = parsed.options["--limit"]; + if (parsed.options["--cursor"] !== undefined) contentOptions.cursor = parsed.options["--cursor"]; + return runContent(dependencies.http, subcommand === "search" ? parsed.positional[0] : undefined, contentOptions, options.timeoutMs); + } + if (command === "docs") { + const [subcommand, ...docsArgs] = rest; + if (subcommand !== "ask") throw usageError("invalid_usage", "Usage: agentcommunity docs ask [options]"); + const parsed = parseNamedOptions(docsArgs, new Set(["--top-k"])); + exactly(parsed.positional, 1, "Usage: agentcommunity docs ask [options]"); + return runDocsAsk(dependencies.http, parsed.positional[0] ?? "", parsed.options["--top-k"], options.timeoutMs); + } + if (command === "batch") { + exactly(rest, 1, "Usage: agentcommunity batch "); + const source = rest[0] ?? ""; + let bytes: Uint8Array; + try { + bytes = source === "-" + ? await dependencies.readStdin(BATCH_INPUT_MAX_BYTES) + : await dependencies.readFile(source, BATCH_INPUT_MAX_BYTES); + } catch { + throw usageError("file_read_error", "Batch input could not be read."); + } + return runBatch(dependencies.http, bytes, options.timeoutMs); + } + throw usageError("unknown_command", `Unknown command: ${command}`); +} + +function errorEnvelope(error: CliError): string { + const body: { error: { code: string; message: string; details?: Record } } = { + error: { code: error.code, message: error.message }, + }; + if (error.details !== undefined) body.error.details = error.details; + return `${JSON.stringify(body)}\n`; +} + +export async function runCli(argv: Array, dependencies: CliDependencies): Promise { + try { + const options = parseGlobals(argv); + const result = await dispatch(options, dependencies); + if (result === null) return 0; + dependencies.stdout(options.json ? `${JSON.stringify(result.payload)}\n` : `${result.human}\n`); + return result.exitCode; + } catch (error) { + const cliError = error instanceof CliError + ? error + : new CliError("network_error", "The Agent Community service could not be reached.", 6); + dependencies.stderr(errorEnvelope(cliError)); + return cliError.exitCode; + } +} + +async function readLimitedFile(path: string, maxBytes = BATCH_INPUT_MAX_BYTES): Promise { + const handle = await open(path, "r"); + try { + const buffer = Buffer.alloc(maxBytes + 1); + const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, 0); + return buffer.subarray(0, bytesRead); + } finally { + await handle.close(); + } +} + +async function readLimitedStdin(maxBytes = BATCH_INPUT_MAX_BYTES): Promise { + const chunks: Array = []; + let total = 0; + for await (const chunk of process.stdin) { + const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + total += bytes.byteLength; + if (total > maxBytes) return Buffer.alloc(maxBytes + 1); + chunks.push(bytes); + } + return Buffer.concat(chunks, total); +} + +function defaultDependencies(): CliDependencies { + const http = new HttpClient(); + const mcp = new McpClient(http, randomUUID, CLI_VERSION); + return { + http, + mcp, + readFile: readLimitedFile, + readStdin: readLimitedStdin, + stdout: (value) => { process.stdout.write(value); }, + stderr: (value) => { process.stderr.write(value); }, + }; +} + +const entryPath = process.argv[1]; +const resolvedEntryUrl = entryPath === undefined ? undefined : pathToFileURL(await realpath(entryPath)).href; +if (resolvedEntryUrl !== undefined && import.meta.url === resolvedEntryUrl) { + process.exitCode = await runCli(process.argv.slice(2), defaultDependencies()); +} diff --git a/src/commands/batch.ts b/src/commands/batch.ts new file mode 100644 index 0000000..6cc9acf --- /dev/null +++ b/src/commands/batch.ts @@ -0,0 +1,30 @@ +import { batchRequestSchema, batchResponseSchema, parseSchema } from "../contracts.js"; +import { BATCH_INPUT_MAX_BYTES } from "../config.js"; +import { CliError, usageError } from "../errors.js"; +import type { HttpTransport } from "../http.js"; + +export async function runBatch(http: HttpTransport, bytes: Uint8Array, timeoutMs: number) { + if (bytes.byteLength > BATCH_INPUT_MAX_BYTES) throw usageError("input_too_large", "Batch input exceeds 262144 bytes."); + let raw: unknown; + try { + raw = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)); + } catch { + throw usageError("invalid_json", "Batch input must be valid UTF-8 JSON."); + } + const parsed = batchRequestSchema.safeParse(raw); + if (!parsed.success) { + const duplicate = parsed.error.issues.some((issue) => issue.message === "duplicate_batch_id"); + const memberOperation = parsed.error.issues.some((issue) => issue.message === "member_operation_forbidden"); + throw usageError(duplicate ? "duplicate_batch_id" : memberOperation ? "member_operation_forbidden" : "invalid_batch", duplicate ? "Batch item IDs must be unique." : memberOperation ? "Member operations are not permitted in batch input." : "Batch input does not match the pinned contract."); + } + const payload = await http.requestJson({ + method: "POST", path: "/api/v1/batch", timeoutMs, maxBytes: 1_048_576, body: parsed.data, + validate: (value) => parseSchema(batchResponseSchema, value), + }); + if (payload.items.length !== parsed.data.items.length || payload.items.some((item, index) => item.id !== parsed.data.items[index]?.id)) { + throw new CliError("batch_order_mismatch", "Batch response did not preserve request order.", 5); + } + const hasError = payload.items.some((item) => item.status === "error"); + const human = payload.items.map((item) => item.status === "ok" ? `${item.id}: ok` : `${item.id}: error (${item.error.code})`).join("\n"); + return { payload, exitCode: hasError ? 8 as const : 0 as const, human }; +} diff --git a/src/commands/content.ts b/src/commands/content.ts new file mode 100644 index 0000000..9242f4b --- /dev/null +++ b/src/commands/content.ts @@ -0,0 +1,39 @@ +import { contentPageSchema, parseSchema } from "../contracts.js"; +import { usageError } from "../errors.js"; +import type { HttpTransport } from "../http.js"; + +export interface ContentOptions { + type?: string; + limit?: string; + cursor?: string; +} + +function buildContentPath(query: string | undefined, options: ContentOptions): string { + if (query !== undefined && query.length > 200) throw usageError("invalid_content_query", "Content query must not exceed 200 characters."); + if (options.type !== undefined && !["docs", "blog", "page"].includes(options.type)) throw usageError("invalid_content_type", "Content type must be docs, blog, or page."); + let limit: number | undefined; + if (options.limit !== undefined) { + if (!/^\d+$/.test(options.limit)) throw usageError("invalid_limit", "Content limit must be an integer from 1 to 50."); + limit = Number(options.limit); + if (limit < 1 || limit > 50) throw usageError("invalid_limit", "Content limit must be an integer from 1 to 50."); + } + if (options.cursor !== undefined && options.cursor.length > 256) throw usageError("invalid_cursor", "Cursor must not exceed 256 characters."); + const params = new URLSearchParams(); + if (query !== undefined) params.set("q", query); + if (options.type !== undefined) params.set("type", options.type); + if (limit !== undefined) params.set("limit", String(limit)); + if (options.cursor !== undefined) params.set("cursor", options.cursor); + const encoded = params.toString(); + return `/api/v1/content${encoded === "" ? "" : `?${encoded}`}`; +} + +export async function runContent(http: HttpTransport, query: string | undefined, options: ContentOptions, timeoutMs: number) { + const payload = await http.requestJson({ + method: "GET", path: buildContentPath(query, options), timeoutMs, maxBytes: 262_144, + validate: (value) => parseSchema(contentPageSchema, value), + }); + const human = payload.items.length === 0 + ? "No content found." + : payload.items.map((item) => `${item.title} (${item.type})\n${item.href}\n${item.description}`).join("\n\n"); + return { payload, exitCode: 0 as const, human }; +} diff --git a/src/commands/docs.ts b/src/commands/docs.ts new file mode 100644 index 0000000..a6bfc03 --- /dev/null +++ b/src/commands/docs.ts @@ -0,0 +1,20 @@ +import { docsAnswerSchema, parseSchema } from "../contracts.js"; +import { usageError } from "../errors.js"; +import type { HttpTransport } from "../http.js"; + +export async function runDocsAsk(http: HttpTransport, query: string, topKValue: string | undefined, timeoutMs: number) { + if (query.length < 2 || query.length > 500) throw usageError("invalid_docs_query", "Documentation question must be from 2 to 500 characters."); + let topK = 5; + if (topKValue !== undefined) { + if (!/^\d+$/.test(topKValue)) throw usageError("invalid_top_k", "top-k must be an integer from 1 to 10."); + topK = Number(topKValue); + if (topK < 1 || topK > 10) throw usageError("invalid_top_k", "top-k must be an integer from 1 to 10."); + } + const payload = await http.requestJson({ + method: "POST", path: "/ask", timeoutMs, maxBytes: 65_536, + body: { query, top_k: topK, streaming: false }, + validate: (value) => parseSchema(docsAnswerSchema, value), + }); + const citations = payload.results.map((result) => `- ${result.name}: ${result.url}`).join("\n"); + return { payload, exitCode: 0 as const, human: citations === "" ? payload.answer : `${payload.answer}\n\nSources:\n${citations}` }; +} diff --git a/src/commands/member.ts b/src/commands/member.ts new file mode 100644 index 0000000..fc1226c --- /dev/null +++ b/src/commands/member.ts @@ -0,0 +1,12 @@ +import { memberSchema, parseSchema } from "../contracts.js"; +import { usageError } from "../errors.js"; +import type { McpTransport } from "../mcp.js"; + +export async function runMember(mcp: McpTransport, query: string, timeoutMs: number) { + if (query.length < 1 || query.length > 200) throw usageError("invalid_member_query", "Member lookup requires an exact name or slug from 1 to 200 characters."); + const payload = await mcp.callTool("lookup_member", { query }, (value) => parseSchema(memberSchema, value), timeoutMs); + const human = payload.matches.length === 0 + ? `Member lookup: ${payload.status}` + : payload.matches.map((match) => `${match.display_name}\n${match.profile_url}${match.member_since === null ? "" : `\nMember since ${match.member_since}`}`).join("\n\n"); + return { payload, exitCode: payload.status === "member" ? 0 as const : 3 as const, human }; +} diff --git a/src/commands/stats.ts b/src/commands/stats.ts new file mode 100644 index 0000000..561c27a --- /dev/null +++ b/src/commands/stats.ts @@ -0,0 +1,7 @@ +import { parseSchema, statsSchema } from "../contracts.js"; +import type { McpTransport } from "../mcp.js"; + +export async function runStats(mcp: McpTransport, timeoutMs: number) { + const payload = await mcp.callTool("get_community_stats", {}, (value) => parseSchema(statsSchema, value), timeoutMs); + return { payload, exitCode: 0 as const, human: `${payload.member_count.toLocaleString("en-US")} members\n${payload.note}` }; +} diff --git a/src/commands/verify.ts b/src/commands/verify.ts new file mode 100644 index 0000000..9762b45 --- /dev/null +++ b/src/commands/verify.ts @@ -0,0 +1,17 @@ +import { certificateSchema, parseSchema } from "../contracts.js"; +import { usageError } from "../errors.js"; +import type { McpTransport } from "../mcp.js"; + +export async function runVerify(mcp: McpTransport, certificateId: string, timeoutMs: number) { + if (certificateId.length < 1 || certificateId.length > 200) throw usageError("invalid_certificate_id", "Certificate ID must be from 1 to 200 characters."); + const payload = await mcp.callTool("verify_certificate", { certificate_id: certificateId }, (value) => parseSchema(certificateSchema, value), timeoutMs); + const exitCode = payload.status === "issued" ? 0 as const + : payload.status === "invalid_format" ? 2 as const + : payload.status === "not_found" ? 3 as const : 6 as const; + const human = [ + `Certificate ${payload.certificate_id}: ${payload.status}`, + payload.agent_name === null ? null : `Agent: ${payload.agent_name}`, + payload.certificate_url, + ].filter((value): value is string => value !== null).join("\n"); + return { payload, exitCode, human }; +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..89947bf --- /dev/null +++ b/src/config.ts @@ -0,0 +1,18 @@ +import { usageError } from "./errors.js"; + +export const AGENT_COMMUNITY_ORIGIN = "https://agentcommunity.org"; +export const AGENT_COMMUNITY_RESOURCE = "https://agentcommunity.org/api"; +export const DEFAULT_TIMEOUT_MS = 10_000; +export const MIN_TIMEOUT_MS = 1_000; +export const MAX_TIMEOUT_MS = 30_000; +export const BATCH_INPUT_MAX_BYTES = 262_144; + +export function parseTimeout(value: string | undefined): number { + if (value === undefined) return DEFAULT_TIMEOUT_MS; + if (!/^\d+$/.test(value)) throw usageError("invalid_timeout", "Timeout must be an integer from 1000 to 30000 milliseconds."); + const timeout = Number(value); + if (timeout < MIN_TIMEOUT_MS || timeout > MAX_TIMEOUT_MS) { + throw usageError("invalid_timeout", "Timeout must be an integer from 1000 to 30000 milliseconds."); + } + return timeout; +} diff --git a/src/contracts.ts b/src/contracts.ts new file mode 100644 index 0000000..f201b31 --- /dev/null +++ b/src/contracts.ts @@ -0,0 +1,137 @@ +import { createHash } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { z } from "zod"; + +import { CliError } from "./errors.js"; + +const httpsUrlSchema = z.string().url().refine((value) => value.startsWith("https://")); +const contentTypeSchema = z.enum(["docs", "blog", "page"]); +const contentItemSchema = z.object({ + title: z.string(), description: z.string(), href: z.string().regex(/^\/(?!\/)/), type: contentTypeSchema, +}).strict(); +const cursorPageSchema = z.object({ + limit: z.number().int().min(1).max(50), next_cursor: z.string().max(256).nullable(), has_more: z.boolean(), +}).strict(); +export const contentPageSchema = z.object({ items: z.array(contentItemSchema).max(50), page: cursorPageSchema }); + +const articleSchema = z.object({ + "@context": z.literal("https://schema.org"), "@id": z.string().url(), "@type": z.literal("Article"), + description: z.string().max(500), name: z.string().max(200), url: z.string().url(), +}).strict(); +export const docsAnswerSchema = z.object({ + _meta: z.object({ version: z.literal("0.55"), response_type: z.enum(["answer", "capability"]), mode: z.literal("list"), site: z.literal("agentcommunity.org") }).strict(), + query: z.string().max(500), answer: z.string().max(1500), + content: z.array(articleSchema).max(10), + results: z.array(z.object({ + url: z.string().url(), site: z.literal("agentcommunity.org"), name: z.string().max(200), + description: z.string().max(500), schema_object: articleSchema, + }).strict()).max(10), +}).strict(); + +export const statsSchema = z.object({ member_count: z.number().int(), note: z.string() }).strict(); +export const memberSchema = z.object({ + status: z.enum(["member", "not_found", "ambiguous"]), + matches: z.array(z.object({ display_name: z.string(), member_since: z.string().date().nullable(), profile_url: z.string().url() }).strict()).max(5), +}).strict(); +export const certificateSchema = z.object({ + certificate_id: z.string(), status: z.enum(["invalid_format", "not_found", "issued", "unavailable"]), + valid_format: z.boolean(), issued: z.boolean().nullable(), agent_name: z.string().nullable(), certificate_url: z.string().url().nullable(), +}).strict(); + +const batchIdSchema = z.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/); +const contentArgumentsSchema = z.object({ + query: z.string().max(200).optional(), type: contentTypeSchema.optional(), limit: z.number().int().min(1).max(50).optional(), cursor: z.string().max(256).optional(), +}).strict(); +const docsArgumentsSchema = z.object({ query: z.string().min(2).max(500), top_k: z.number().int().min(1).max(10).optional() }).strict(); +const batchEnvelopeItemSchema = z.object({ id: batchIdSchema, operation: z.string().min(1).max(128), arguments: z.unknown() }).strict(); +export const batchRequestSchema = z.object({ items: z.array(batchEnvelopeItemSchema).min(1).max(10) }).strict().superRefine((value, context) => { + const ids = new Set(); + for (const [index, item] of value.items.entries()) { + if (ids.has(item.id)) context.addIssue({ code: "custom", message: "duplicate_batch_id", path: ["items", index, "id"] }); + ids.add(item.id); + if (item.operation === "content.list" && !contentArgumentsSchema.safeParse(item.arguments).success) { + context.addIssue({ code: "custom", message: "invalid_known_arguments", path: ["items", index, "arguments"] }); + } + if (item.operation === "docs.ask" && !docsArgumentsSchema.safeParse(item.arguments).success) { + context.addIssue({ code: "custom", message: "invalid_known_arguments", path: ["items", index, "arguments"] }); + } + if (item.operation === "lookup_member" || item.operation.startsWith("member.")) { + context.addIssue({ code: "custom", message: "member_operation_forbidden", path: ["items", index, "operation"] }); + } + } +}); + +const batchErrorSchema = z.object({ + code: z.enum(["unknown_operation", "invalid_arguments", "operation_failed", "deadline_exceeded", "response_too_large", "total_response_too_large"]), + message: z.string(), +}).strict(); +const batchDocsSourceSchema = z.object({ + title: z.string().max(200), description: z.string().max(500), path: z.string().regex(/^\/(?!\/)/), + url: z.string().url(), excerpt: z.string().max(320), +}).strict(); +const batchContentOkSchema = z.object({ id: batchIdSchema, operation: z.literal("content.list"), status: z.literal("ok"), result: contentPageSchema.strict() }).strict(); +const batchDocsOkSchema = z.object({ + id: batchIdSchema, operation: z.literal("docs.ask"), status: z.literal("ok"), + result: z.object({ query: z.string().max(500), answer: z.string().max(1500), sources: z.array(batchDocsSourceSchema).max(10) }).strict(), +}).strict(); +const batchFailedSchema = z.object({ id: batchIdSchema, operation: z.string().min(1).max(128), status: z.literal("error"), error: batchErrorSchema }).strict(); +export const batchResponseSchema = z.object({ items: z.array(z.union([batchContentOkSchema, batchDocsOkSchema, batchFailedSchema])).min(1).max(10) }).strict(); + +export function parseSchema(schema: z.ZodType, value: unknown): T { + return schema.parse(value); +} + +interface ContractLock { + bundle_version: string; + compatible_range: string; + manifest_url: string; + manifest_sha256: string; +} + +const lockSchema = z.object({ + bundle_version: z.literal("1.0.0"), compatible_range: z.literal("^1.0.0"), + manifest_url: z.literal("https://agentcommunity.org/.well-known/agentcommunity-contracts/1.0.0/manifest.json"), + manifest_sha256: z.string().regex(/^sha256:[0-9a-f]{64}$/), +}).strict(); +const manifestFileSchema = z.object({ + path: z.enum(["auth.json", "batch.json", "mcp.json", "openapi.json", "rest.json"]), + media_type: z.literal("application/json"), bytes: z.number().int().nonnegative(), sha256: z.string().regex(/^sha256:[0-9a-f]{64}$/), +}).strict(); +const manifestSchema = z.object({ + format_version: z.literal(1), bundle_version: z.literal("1.0.0"), page_source: z.literal("agentcommunity-page-openapi@1.3.0"), + openapi_version: z.literal("1.3.0"), workos_auth_md_commit: z.string().regex(/^[0-9a-f]{40}$/), contract_mode: z.literal("public"), + files: z.array(manifestFileSchema).length(5), +}).strict(); + +function sha256(bytes: Uint8Array): string { + return `sha256:${createHash("sha256").update(bytes).digest("hex")}`; +} + +async function readJson(url: URL): Promise { + return JSON.parse(await readFile(url, "utf8")); +} + +export async function verifyContractDirectory(repositoryRoot: URL): Promise<{ bundleVersion: string; manifestSha256: string }> { + try { + const lockUrl = new URL("contracts/page.lock.json", repositoryRoot); + const lock = lockSchema.parse(await readJson(lockUrl)) as ContractLock; + if (!httpsUrlSchema.safeParse(lock.manifest_url).success || lock.manifest_url.includes("..")) throw new Error("unsafe manifest URL"); + const bundleRoot = new URL(`contracts/page/${lock.bundle_version}/`, repositoryRoot); + const manifestBytes = await readFile(new URL("manifest.json", bundleRoot)); + if (sha256(manifestBytes) !== lock.manifest_sha256) throw new Error("manifest hash mismatch"); + const detached = await readFile(new URL("manifest.sha256", bundleRoot), "utf8"); + if (detached !== `${lock.manifest_sha256.slice(7)} manifest.json\n`) throw new Error("detached hash mismatch"); + const manifest = manifestSchema.parse(JSON.parse(manifestBytes.toString("utf8"))); + const expectedPaths = ["auth.json", "batch.json", "mcp.json", "openapi.json", "rest.json"]; + if (manifest.files.map((file) => file.path).join("\n") !== expectedPaths.join("\n")) throw new Error("invalid inventory"); + for (const file of manifest.files) { + if (file.path.includes("/") || file.path.includes("..")) throw new Error("path escape"); + const bytes = await readFile(new URL(file.path, bundleRoot)); + if (bytes.byteLength !== file.bytes || sha256(bytes) !== file.sha256) throw new Error(`payload mismatch: ${file.path}`); + JSON.parse(bytes.toString("utf8")); + } + return { bundleVersion: lock.bundle_version, manifestSha256: lock.manifest_sha256 }; + } catch { + throw new CliError("contract_mismatch", "The vendored PAGE contract bundle failed verification.", 5); + } +} diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..b2ed5c9 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,19 @@ +export type ExitCode = 0 | 2 | 3 | 4 | 5 | 6 | 7 | 8; + +export class CliError extends Error { + readonly code: string; + readonly exitCode: ExitCode; + readonly details: Record | undefined; + + constructor(code: string, message: string, exitCode: ExitCode, details?: Record) { + super(message); + this.name = "CliError"; + this.code = code; + this.exitCode = exitCode; + this.details = details; + } +} + +export function usageError(code: string, message: string): CliError { + return new CliError(code, message, 2); +} diff --git a/src/http.ts b/src/http.ts new file mode 100644 index 0000000..d11ae97 --- /dev/null +++ b/src/http.ts @@ -0,0 +1,129 @@ +import { AGENT_COMMUNITY_ORIGIN } from "./config.js"; +import { CliError } from "./errors.js"; + +export interface JsonRequest { + method: "GET" | "POST"; + path: string; + timeoutMs: number; + maxBytes: number; + headers?: Record; + body?: unknown; + validate(value: unknown): T; +} + +export interface HttpTransport { + requestJson(request: JsonRequest): Promise; +} + +const MAX_RETRY_AFTER_MS = 300_000; + +function retryAfterDetails(value: string | null, now: number): Record | undefined { + if (value === null) return undefined; + let milliseconds: number; + if (/^\d+$/.test(value)) { + milliseconds = Number(value) * 1_000; + } else { + const date = Date.parse(value); + if (!Number.isFinite(date)) return undefined; + milliseconds = Math.max(0, date - now); + } + if (!Number.isSafeInteger(milliseconds) || milliseconds < 0 || milliseconds > MAX_RETRY_AFTER_MS) return undefined; + return { retry_after_ms: milliseconds }; +} + +function isJsonMime(value: string | null): boolean { + return value !== null && /^application\/json(?:\s*;\s*charset=utf-8)?$/i.test(value.trim()); +} + +async function readBounded(response: Response, maxBytes: number): Promise { + const declared = response.headers.get("content-length"); + if (declared !== null && (!/^\d+$/.test(declared) || Number(declared) > maxBytes)) { + throw new CliError("response_too_large", "The service response exceeded the allowed size.", 5); + } + if (response.body === null) return new Uint8Array(); + const reader = response.body.getReader(); + const chunks: Array = []; + let total = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + total += value.byteLength; + if (total > maxBytes) { + await reader.cancel(); + throw new CliError("response_too_large", "The service response exceeded the allowed size.", 5); + } + chunks.push(value); + } + const bytes = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + return bytes; +} + +export class HttpClient implements HttpTransport { + constructor( + private readonly fetchImpl: typeof fetch = fetch, + private readonly now: () => number = Date.now, + ) {} + + async requestJson(request: JsonRequest): Promise { + if (!request.path.startsWith("/") || request.path.startsWith("//")) { + throw new CliError("invalid_path", "The request path is invalid.", 2); + } + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), request.timeoutMs); + try { + const headers: Record = { Accept: "application/json", ...request.headers }; + const init: RequestInit = { method: request.method, headers, redirect: "manual", signal: controller.signal }; + if (request.body !== undefined) { + headers["Content-Type"] = "application/json"; + init.body = JSON.stringify(request.body); + } + const response = await this.fetchImpl(`${AGENT_COMMUNITY_ORIGIN}${request.path}`, init); + if (response.status >= 300 && response.status < 400) { + throw new CliError("redirect_rejected", "The service returned an unexpected redirect.", 5); + } + if (response.status === 429) { + throw new CliError( + "rate_limited", + "The service rate limit was reached.", + 7, + retryAfterDetails(response.headers.get("retry-after"), this.now()), + ); + } + if (response.status >= 500) { + throw new CliError("upstream_unavailable", "The Agent Community service is temporarily unavailable.", 6); + } + if (response.status < 200 || response.status >= 300) { + throw new CliError("remote_error", "The service rejected the request.", 5); + } + if (!isJsonMime(response.headers.get("content-type"))) { + throw new CliError("invalid_content_type", "The service returned an unexpected content type.", 5); + } + const bytes = await readBounded(response, request.maxBytes); + let value: unknown; + try { + value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)); + } catch { + throw new CliError("invalid_json", "The service returned invalid JSON.", 5); + } + try { + return request.validate(value); + } catch (error) { + if (error instanceof CliError) throw error; + throw new CliError("schema_mismatch", "The service response did not match the pinned contract.", 5); + } + } catch (error) { + if (error instanceof CliError) throw error; + if (error instanceof DOMException && error.name === "AbortError") { + throw new CliError("timeout", "The request timed out.", 6); + } + throw new CliError("network_error", "The Agent Community service could not be reached.", 6); + } finally { + clearTimeout(timer); + } + } +} diff --git a/src/mcp.ts b/src/mcp.ts new file mode 100644 index 0000000..dd923f5 --- /dev/null +++ b/src/mcp.ts @@ -0,0 +1,96 @@ +import { CliError } from "./errors.js"; +import type { HttpTransport } from "./http.js"; +import { isDeepStrictEqual } from "node:util"; + +export const MCP_PROTOCOL_VERSION = "2026-07-28"; +export const PRODUCT_TOOL_NAMES = ["lookup_member", "get_community_stats", "register_agent", "verify_certificate"] as const; +export type ReadOnlyToolName = "lookup_member" | "get_community_stats" | "verify_certificate"; + +export interface McpTransport { + callTool(name: ReadOnlyToolName, argumentsValue: Record, validate: (value: unknown) => T, timeoutMs?: number): Promise; +} + +interface McpEnvelope { + jsonrpc: "2.0"; + id: string; + result?: { + resultType?: unknown; + content?: unknown; + structuredContent?: unknown; + _meta?: unknown; + }; + error?: unknown; +} + +function validateEnvelope(value: unknown): McpEnvelope { + if (typeof value !== "object" || value === null) throw new Error("not an object"); + const envelope = value as Record; + if (envelope.jsonrpc !== "2.0" || typeof envelope.id !== "string") throw new Error("invalid envelope"); + return envelope as unknown as McpEnvelope; +} + +export class McpClient implements McpTransport { + constructor( + private readonly http: HttpTransport, + private readonly idFactory: () => string, + private readonly version: string, + ) {} + + async callTool(name: ReadOnlyToolName, argumentsValue: Record, validate: (value: unknown) => T, timeoutMs = 10_000): Promise { + if (!(["lookup_member", "get_community_stats", "verify_certificate"] as Array).includes(name)) { + throw new CliError("unsupported_tool", "Only the three read-only MCP tools are available.", 2); + } + const id = this.idFactory(); + const response = await this.http.requestJson({ + method: "POST", + path: "/mcp", + timeoutMs, + maxBytes: 262_144, + headers: { + "MCP-Protocol-Version": MCP_PROTOCOL_VERSION, + "Mcp-Method": "tools/call", + "Mcp-Name": name, + }, + body: { + jsonrpc: "2.0", + id, + method: "tools/call", + params: { + _meta: { + "io.modelcontextprotocol/clientCapabilities": {}, + "io.modelcontextprotocol/clientInfo": { name: "@agentcommunity/cli", version: this.version }, + "io.modelcontextprotocol/protocolVersion": MCP_PROTOCOL_VERSION, + }, + arguments: argumentsValue, + name, + }, + }, + validate: validateEnvelope, + }); + if (response.id !== id || response.error !== undefined || response.result === undefined) { + throw new CliError("mcp_protocol_error", "The MCP service returned an invalid response.", 5); + } + const result = response.result; + if (result.resultType !== "complete" || !Array.isArray(result.content) || result.content.length !== 1 || typeof result._meta !== "object" || result._meta === null || !("io.modelcontextprotocol/serverInfo" in result._meta)) { + throw new CliError("mcp_protocol_error", "The MCP service returned an invalid modern result.", 5); + } + const content = result.content[0]; + if (typeof content !== "object" || content === null || (content as Record).type !== "text" || typeof (content as Record).text !== "string") { + throw new CliError("mcp_protocol_error", "The MCP service returned invalid text content.", 5); + } + let textPayload: unknown; + try { + textPayload = JSON.parse((content as { text: string }).text); + } catch { + throw new CliError("mcp_protocol_error", "The MCP text content was not valid JSON.", 5); + } + if (!isDeepStrictEqual(textPayload, result.structuredContent)) { + throw new CliError("mcp_protocol_error", "The MCP text and structured results disagreed.", 5); + } + try { + return validate(result.structuredContent); + } catch { + throw new CliError("schema_mismatch", "The MCP structured result did not match the pinned contract.", 5); + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..82e4820 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "types": ["node", "vitest/globals"] + }, + "include": ["src", "scripts", "vitest.config.ts", "tsup.config.ts", "eslint.config.js"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..12b84ff --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/cli.ts"], + format: ["esm"], + platform: "node", + target: "node22", + clean: true, + bundle: true, + sourcemap: false, + splitting: false, + banner: { js: "#!/usr/bin/env node" }, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..fbac359 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + coverage: { enabled: false }, + environment: "node", + }, +}); From b1ff7720d4cc46f970acbdf942d1093b01efa8c1 Mon Sep 17 00:00:00 2001 From: nembal Date: Sun, 2 Aug 2026 02:41:46 +0700 Subject: [PATCH 2/4] fix: close CLI protocol validation gaps --- README.md | 2 +- src/__tests__/commands.test.ts | 21 ++++++++++++++------- src/__tests__/mcp.test.ts | 29 ++++++++++++++++++++++++++++- src/commands/batch.ts | 11 +++++++---- src/contracts.ts | 4 ++-- src/mcp.ts | 11 +++++++++++ 6 files changed, 63 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 275b4de..b057c62 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ agentcommunity batch Every command accepts `--json` and `--timeout `. The per-call timeout defaults to 10,000 ms and must be between 1,000 and 30,000 ms. `--json` writes exactly one JSON value followed by LF. Human-readable output is the default and honors `NO_COLOR` (the CLI currently emits no ANSI color). Local, network, and protocol errors write one stable JSON error envelope to stderr and nothing to stdout. Semantic-negative service results still print their payload and return a nonzero status. -`stats` calls only modern MCP `get_community_stats`. `member` is an exact name-or-slug lookup through `lookup_member`; it never enumerates the directory or falls back to content or map search. `verify` calls only `verify_certificate`. `content list` and `content search` use `/api/v1/content`; an empty page is successful. `docs ask` posts a non-streaming request directly to `/ask`. `batch` accepts a strict JSON file or `-` for stdin, caps input at 262,144 bytes before parsing, permits only `content.list` and `docs.ask`, and preserves item order. +`stats` calls only modern MCP `get_community_stats`. `member` is an exact name-or-slug lookup through `lookup_member`; it never enumerates the directory or falls back to content or map search. `verify` calls only `verify_certificate`. `content list` and `content search` use `/api/v1/content`; an empty page is successful. `docs ask` posts a non-streaming request directly to `/ask`. `batch` accepts a strict JSON file or `-` for stdin, caps input at 262,144 bytes before parsing, and locally permits only `content.list` and `docs.ask` with their closed argument schemas. Unknown/member/registration operations and URL/header/credential-bearing argument escapes are rejected before network access; responses must preserve each item's ordered ID and operation. For write-capable certificate registration use [@agentcommunity/dmv-agent](https://www.npmjs.com/package/@agentcommunity/dmv-agent). For AID diagnostics use [@agentcommunity/aid-doctor](https://www.npmjs.com/package/@agentcommunity/aid-doctor). Their behavior is intentionally not copied into this umbrella CLI. diff --git a/src/__tests__/commands.test.ts b/src/__tests__/commands.test.ts index 2a43a76..48144a9 100644 --- a/src/__tests__/commands.test.ts +++ b/src/__tests__/commands.test.ts @@ -102,21 +102,28 @@ describe("the seven read-only commands", () => { test.each([ [{ items: [{ id: "same", operation: "content.list", arguments: {} }, { id: "same", operation: "content.list", arguments: {} }] }, "duplicate_batch_id"], - [{ items: [{ id: "x", operation: "member.lookup", arguments: {} }] }, "member_operation_forbidden"], - ])("rejects malformed or member-capable batch envelopes locally", async (input, code) => { + [{ items: [{ id: "x", operation: "member.lookup", arguments: {} }] }, "unknown_operation"], + [{ items: [{ id: "x", operation: "register_agent", arguments: { agent_name: "fixture", email: "fixture@example.com" } }] }, "unknown_operation"], + [{ items: [{ id: "x", operation: "future.read", arguments: { url: "https://evil.example", headers: { Authorization: "Bearer fixture-secret" }, credential: "sk_live_fixture_secret" } }] }, "unknown_operation"], + [{ items: [{ id: "x", operation: "content.list", arguments: { url: "https://evil.example" } }] }, "invalid_batch"], + [{ items: [{ id: "x", operation: "docs.ask", arguments: { query: "Valid question?", headers: { Authorization: "Bearer fixture-secret" } } }] }, "invalid_batch"], + [{ items: [{ id: "x", operation: "content.list" }] }, "invalid_batch"], + [{ items: [{ id: "x", operation: "content.list", arguments: {}, url: "https://evil.example", headers: {}, credentials: "fixture-secret" }] }, "invalid_batch"], + ])("rejects every operation or argument outside the closed batch contract locally", async (input, code) => { const batchHarness = harness({ readFile: vi.fn().mockResolvedValue(Buffer.from(JSON.stringify(input))) }); expect(await runCli(["batch", "input.json"], batchHarness.dependencies)).toBe(2); expect(JSON.parse(batchHarness.output().stderr)).toMatchObject({ error: { code } }); expect(batchHarness.dependencies.http.requestJson).not.toHaveBeenCalled(); }); - test("posts an unknown non-member operation unchanged for the contract-defined item error", async () => { - const request = { items: [{ id: "x", operation: "future.read", arguments: { fixture: true } }] }; - const response = { items: [{ id: "x", operation: "future.read", status: "error", error: { code: "unknown_operation", message: "Unsupported operation" } }] }; + test("rejects a same-ID response whose operation does not match the ordered request item", async () => { + const request = { items: [{ id: "x", operation: "content.list", arguments: {} }] }; + const response = { items: [{ id: "x", operation: "docs.ask", status: "ok", result: { query: "What is AID?", answer: "Fixture", sources: [] } }] }; const requestJson = vi.fn().mockResolvedValue(response); const batchHarness = harness({ http: { requestJson }, readFile: vi.fn().mockResolvedValue(Buffer.from(JSON.stringify(request))) }); - expect(await runCli(["batch", "input.json", "--json"], batchHarness.dependencies)).toBe(8); - expect(requestJson).toHaveBeenCalledWith(expect.objectContaining({ body: request })); + expect(await runCli(["batch", "input.json", "--json"], batchHarness.dependencies)).toBe(5); + expect(batchHarness.output().stdout).toBe(""); + expect(JSON.parse(batchHarness.output().stderr)).toMatchObject({ error: { code: "batch_correlation_mismatch" } }); }); test("maps negative member and certificate states to stable exits while printing payloads", async () => { diff --git a/src/__tests__/mcp.test.ts b/src/__tests__/mcp.test.ts index 3edf1cc..c6f98d6 100644 --- a/src/__tests__/mcp.test.ts +++ b/src/__tests__/mcp.test.ts @@ -2,12 +2,20 @@ import { describe, expect, test, vi } from "vitest"; import { McpClient, MCP_PROTOCOL_VERSION, PRODUCT_TOOL_NAMES } from "../mcp.js"; +const exactServerInfo = { + icons: [{ mimeType: "image/png", sizes: ["180x180"], src: "https://agentcommunity.org/apple-touch-icon.png" }], + name: "agentcommunity", + title: "Agent Community", + version: "1.0.0", + websiteUrl: "https://agentcommunity.org", +}; + describe("modern MCP transport", () => { test("sends the exact modern body and headers without tools/list", async () => { const requestJson = vi.fn().mockResolvedValue({ jsonrpc: "2.0", id: "test-id", result: { resultType: "complete", content: [{ type: "text", text: '{"member_count":1,"note":"fixture"}' }], - structuredContent: { member_count: 1, note: "fixture" }, _meta: { "io.modelcontextprotocol/serverInfo": { name: "agentcommunity" } }, + structuredContent: { member_count: 1, note: "fixture" }, _meta: { "io.modelcontextprotocol/serverInfo": exactServerInfo }, }, }); const client = new McpClient({ requestJson }, () => "test-id", "0.1.0"); @@ -50,4 +58,23 @@ describe("modern MCP transport", () => { }); await expect(client.callTool("get_community_stats", {}, (value) => value)).rejects.toMatchObject({ exitCode: 5 }); }); + + test.each([ + null, + { name: "agentcommunity" }, + { ...exactServerInfo, websiteUrl: "https://evil.example" }, + { ...exactServerInfo, icons: [] }, + { ...exactServerInfo, unexpected: true }, + ])("rejects null, malformed, or wrong modern serverInfo: %j", async (serverInfo) => { + const requestJson = vi.fn().mockResolvedValue({ + jsonrpc: "2.0", id: "test-id", result: { + resultType: "complete", + _meta: { "io.modelcontextprotocol/serverInfo": serverInfo }, + structuredContent: { member_count: 1, note: "fixture" }, + content: [{ type: "text", text: '{"member_count":1,"note":"fixture"}' }], + }, + }); + const client = new McpClient({ requestJson }, () => "test-id", "0.1.0"); + await expect(client.callTool("get_community_stats", {}, (value) => value)).rejects.toMatchObject({ exitCode: 5, code: "mcp_protocol_error" }); + }); }); diff --git a/src/commands/batch.ts b/src/commands/batch.ts index 6cc9acf..eb5e9e1 100644 --- a/src/commands/batch.ts +++ b/src/commands/batch.ts @@ -14,15 +14,18 @@ export async function runBatch(http: HttpTransport, bytes: Uint8Array, timeoutMs const parsed = batchRequestSchema.safeParse(raw); if (!parsed.success) { const duplicate = parsed.error.issues.some((issue) => issue.message === "duplicate_batch_id"); - const memberOperation = parsed.error.issues.some((issue) => issue.message === "member_operation_forbidden"); - throw usageError(duplicate ? "duplicate_batch_id" : memberOperation ? "member_operation_forbidden" : "invalid_batch", duplicate ? "Batch item IDs must be unique." : memberOperation ? "Member operations are not permitted in batch input." : "Batch input does not match the pinned contract."); + const unknownOperation = parsed.error.issues.some((issue) => issue.message === "unknown_operation"); + throw usageError(duplicate ? "duplicate_batch_id" : unknownOperation ? "unknown_operation" : "invalid_batch", duplicate ? "Batch item IDs must be unique." : unknownOperation ? "Batch operation must be content.list or docs.ask." : "Batch input does not match the pinned contract."); } const payload = await http.requestJson({ method: "POST", path: "/api/v1/batch", timeoutMs, maxBytes: 1_048_576, body: parsed.data, validate: (value) => parseSchema(batchResponseSchema, value), }); - if (payload.items.length !== parsed.data.items.length || payload.items.some((item, index) => item.id !== parsed.data.items[index]?.id)) { - throw new CliError("batch_order_mismatch", "Batch response did not preserve request order.", 5); + if (payload.items.length !== parsed.data.items.length || payload.items.some((item, index) => { + const requestItem = parsed.data.items[index]; + return requestItem === undefined || item.id !== requestItem.id || item.operation !== requestItem.operation; + })) { + throw new CliError("batch_correlation_mismatch", "Batch response did not preserve request ID and operation order.", 5); } const hasError = payload.items.some((item) => item.status === "error"); const human = payload.items.map((item) => item.status === "ok" ? `${item.id}: ok` : `${item.id}: error (${item.error.code})`).join("\n"); diff --git a/src/contracts.ts b/src/contracts.ts index f201b31..f982180 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -55,8 +55,8 @@ export const batchRequestSchema = z.object({ items: z.array(batchEnvelopeItemSch if (item.operation === "docs.ask" && !docsArgumentsSchema.safeParse(item.arguments).success) { context.addIssue({ code: "custom", message: "invalid_known_arguments", path: ["items", index, "arguments"] }); } - if (item.operation === "lookup_member" || item.operation.startsWith("member.")) { - context.addIssue({ code: "custom", message: "member_operation_forbidden", path: ["items", index, "operation"] }); + if (item.operation !== "content.list" && item.operation !== "docs.ask") { + context.addIssue({ code: "custom", message: "unknown_operation", path: ["items", index, "operation"] }); } } }); diff --git a/src/mcp.ts b/src/mcp.ts index dd923f5..1856e10 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -6,6 +6,14 @@ export const MCP_PROTOCOL_VERSION = "2026-07-28"; export const PRODUCT_TOOL_NAMES = ["lookup_member", "get_community_stats", "register_agent", "verify_certificate"] as const; export type ReadOnlyToolName = "lookup_member" | "get_community_stats" | "verify_certificate"; +const EXPECTED_SERVER_INFO = { + icons: [{ mimeType: "image/png", sizes: ["180x180"], src: "https://agentcommunity.org/apple-touch-icon.png" }], + name: "agentcommunity", + title: "Agent Community", + version: "1.0.0", + websiteUrl: "https://agentcommunity.org", +}; + export interface McpTransport { callTool(name: ReadOnlyToolName, argumentsValue: Record, validate: (value: unknown) => T, timeoutMs?: number): Promise; } @@ -74,6 +82,9 @@ export class McpClient implements McpTransport { if (result.resultType !== "complete" || !Array.isArray(result.content) || result.content.length !== 1 || typeof result._meta !== "object" || result._meta === null || !("io.modelcontextprotocol/serverInfo" in result._meta)) { throw new CliError("mcp_protocol_error", "The MCP service returned an invalid modern result.", 5); } + if (!isDeepStrictEqual((result._meta as Record)["io.modelcontextprotocol/serverInfo"], EXPECTED_SERVER_INFO)) { + throw new CliError("mcp_protocol_error", "The MCP service identity did not match the pinned contract.", 5); + } const content = result.content[0]; if (typeof content !== "object" || content === null || (content as Record).type !== "text" || typeof (content as Record).text !== "string") { throw new CliError("mcp_protocol_error", "The MCP service returned invalid text content.", 5); From 824f729e507ed712c88d575c666092170f1cea5f Mon Sep 17 00:00:00 2001 From: nembal Date: Sun, 2 Aug 2026 03:41:52 +0700 Subject: [PATCH 3/4] feat(auth): add user-claimed CLI authorization --- AGENTS.md | 15 +- README.md | 33 +- SECURITY.md | 6 +- package.json | 2 +- scripts/audit-package.ts | 9 +- src/__tests__/commands.test.ts | 7 +- src/__tests__/http.test.ts | 31 ++ src/__tests__/package-boundary.test.ts | 6 + src/auth/__tests__/auth-commands.test.ts | 191 +++++++++ src/auth/__tests__/cli-auth.test.ts | 184 +++++++++ src/auth/__tests__/credential-store.test.ts | 243 +++++++++++ src/auth/__tests__/device-flow.test.ts | 207 ++++++++++ src/auth/__tests__/discovery.test.ts | 147 +++++++ src/auth/credential-store.ts | 430 ++++++++++++++++++++ src/auth/device-flow.ts | 272 +++++++++++++ src/auth/discovery.ts | 171 ++++++++ src/cli.ts | 112 ++++- src/commands/auth.ts | 268 ++++++++++++ src/http.ts | 63 ++- 19 files changed, 2374 insertions(+), 23 deletions(-) create mode 100644 src/auth/__tests__/auth-commands.test.ts create mode 100644 src/auth/__tests__/cli-auth.test.ts create mode 100644 src/auth/__tests__/credential-store.test.ts create mode 100644 src/auth/__tests__/device-flow.test.ts create mode 100644 src/auth/__tests__/discovery.test.ts create mode 100644 src/auth/credential-store.ts create mode 100644 src/auth/device-flow.ts create mode 100644 src/auth/discovery.ts create mode 100644 src/commands/auth.ts diff --git a/AGENTS.md b/AGENTS.md index 79cd793..24887d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,9 +2,9 @@ ## Product boundary -This repository owns one public TypeScript/ESM package, `@agentcommunity/cli`, and one binary, `agentcommunity`. V1 is read-only, CLI-only, and supports macOS/Linux on Node `^22.14.0 || ^24.0.0 || ^26.0.0`. Do not add a JavaScript SDK export, Windows support claim, workspace coupling to PAGE, auth commands, registration, payment behavior, telemetry, an update ping, or a runtime `--base-url` without a separately approved task. +This repository owns one public TypeScript/ESM package, `@agentcommunity/cli`, and one binary, `agentcommunity`. V1 is CLI-only and supports macOS/Linux on Node `^22.14.0 || ^24.0.0 || ^26.0.0`. Do not add a JavaScript SDK export, Windows support claim, workspace coupling to PAGE, registration, payment behavior, telemetry, an update ping, or a runtime `--base-url` without a separately approved task. -The seven commands are `stats`, `member`, `verify`, `content list`, `content search`, `docs ask`, and `batch`. `register_agent` is catalog evidence only: no runtime path may call it. Link the specialist `@agentcommunity/dmv-agent` and `@agentcommunity/aid-doctor` instead of wrapping or copying them. +The seven public-data commands are `stats`, `member`, `verify`, `content list`, `content search`, `docs ask`, and `batch`. User-claimed authorization adds `auth login`, `auth status`, local-only `auth logout`, and current-access-token-only `auth revoke`. `register_agent` is catalog evidence only: no runtime path may call it. Link the specialist `@agentcommunity/dmv-agent` and `@agentcommunity/aid-doctor` instead of wrapping or copying them. ## Architecture @@ -13,8 +13,9 @@ The seven commands are `stats`, `member`, `verify`, `content list`, `content sea - `src/mcp.ts` is a narrow modern `2026-07-28` client. Runtime commands call one tool directly and never add a `tools/list` round trip. - `src/commands/` modules orchestrate injected HTTP/MCP/filesystem/output boundaries and return typed results. - `src/contracts.ts` validates runtime payloads and the vendored PAGE bundle. Contract scripts may fetch only for explicit maintenance; install and normal execution remain offline except for the requested command. +- `src/auth/discovery.ts` owns fail-closed path PRM/AS validation. `src/auth/device-flow.ts` owns the in-memory WorkOS `service_auth` ceremony. `src/auth/credential-store.ts` owns POSIX path, mode, ownership, locking, and atomic-write safety. Never bypass these layers or persist ceremony values. -Stable exits are: `0` success, `2` usage/local input, `3` domain-negative, `4` reserved for auth, `5` protocol/schema/contract, `6` timeout/unavailable, `7` rate limit, and `8` mixed batch. +Stable exits are: `0` success, `2` usage/local input, `3` domain-negative, `4` auth/credential safety, `5` protocol/schema/contract, `6` timeout/unavailable, `7` rate limit, and `8` mixed batch. ## Contract policy @@ -38,6 +39,10 @@ The final package audit must inspect the exact tarball allowlist and metadata, s ## Security and release gates -Never log request bodies, credentials, or token-like values. Do not add production credentials to tests or CI. Keep package install scripts absent. Production URLs remain exact HTTPS Agent Community URLs; reject redirects and path escapes. Batch input contains no item URL, headers, credentials, or member operations. +Never log request bodies, credentials, or token-like values. The intentional verification URI/user-code progress event is the only ceremony-output exception and must occur before polling. Do not add production credentials to tests or CI. Keep package install scripts absent. Production URLs remain exact HTTPS Agent Community URLs; reject redirects and path escapes. Batch input contains no item URL, headers, credentials, or member operations. -There is intentionally no `release.yml`. Do not publish, push, deploy, create credentials, or add OIDC permissions without explicit owner authorization. Keep these states distinct in docs and reports: source complete, npm package published, PAGE endpoint deployed/production-capable, PAGE linked/discoverable. The current batch source awaits PAGE production deployment. +Auth discovery always starts from an unauthenticated exact `/api` challenge and validates the path PRM, issuer, protected resource, scopes, service-auth declarations, grants, and endpoint origins before sending any secret. `auth status` is live; refresh uses only RFC 7523 JWT bearer. `auth revoke` means RFC 7009 processing of the current access token only and never delegation cancellation. PAGE members UI owns delegation management. + +The credential store is POSIX-only. Require user-owned non-symlink `0700` directories and user-owned regular single-link `0600` files; reject unsafe parents, modes, owners, symlinks, hardlinks, and locks. Preserve bounded locking, conservative stale-lock checks, same-directory exclusive/no-follow temp creation, fsync-before-rename, atomic rename, directory fsync, conditional refresh/removal, and cleanup on interruption. + +There is intentionally no `release.yml`. Do not publish, push, deploy, create credentials, run live auth, or add OIDC permissions without explicit owner authorization. Keep these states distinct in docs and reports: source complete, npm package published, PAGE endpoint deployed/production-capable, PAGE linked/discoverable. The current batch and agent-auth source await PAGE production deployment. A live auth smoke additionally requires an owner-authorized dedicated test account. diff --git a/README.md b/README.md index b057c62..b5d3229 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Agent Community CLI -`@agentcommunity/cli` is the standalone, read-only command-line client for Agent Community's public agent interfaces. It provides seven commands and does not expose a public JavaScript SDK. +`@agentcommunity/cli` is the standalone command-line client for Agent Community's public agent interfaces. It provides seven read-only public-data commands plus user-claimed authorization commands and does not expose a public JavaScript SDK. -The source is complete for macOS and Linux on Node.js `^22.14.0 || ^24.0.0 || ^26.0.0`. The package is not yet published, PAGE has not yet linked it, and the branch-local batch endpoint must be deployed before `batch` is production-capable. Do not treat source completion as npm publication, production availability, or Agent Community discovery linkage. +The source is complete for macOS and Linux on Node.js `^22.14.0 || ^24.0.0 || ^26.0.0`. The package is not yet published, PAGE has not yet linked it, and the branch-local batch and agent-authorization endpoints must be deployed before those commands are production-capable. Do not treat source completion as npm publication, production availability, or Agent Community discovery linkage. ## Source checkout usage @@ -26,12 +26,33 @@ agentcommunity content list [--type docs|blog|page] [--limit 1..50] [--cursor op agentcommunity content search [--type docs|blog|page] [--limit 1..50] [--cursor opaque] agentcommunity docs ask [--top-k 1..10] agentcommunity batch +agentcommunity auth login --login-hint [--scope ...] +agentcommunity auth status +agentcommunity auth logout +agentcommunity auth revoke ``` -Every command accepts `--json` and `--timeout `. The per-call timeout defaults to 10,000 ms and must be between 1,000 and 30,000 ms. `--json` writes exactly one JSON value followed by LF. Human-readable output is the default and honors `NO_COLOR` (the CLI currently emits no ANSI color). Local, network, and protocol errors write one stable JSON error envelope to stderr and nothing to stdout. Semantic-negative service results still print their payload and return a nonzero status. +Every command accepts `--json`; every remote command accepts `--timeout `. Local-only `auth logout` deliberately does not accept `--timeout`. The per-call timeout defaults to 10,000 ms and must be between 1,000 and 30,000 ms. `--json` writes exactly one final JSON value followed by LF to stdout. Human-readable output is the default and honors `NO_COLOR` (the CLI currently emits no ANSI color). Local, network, and protocol errors write one stable JSON error envelope to stderr and nothing to stdout. Semantic-negative service results still print their payload and return a nonzero status. + +`auth login` has one necessary ceremony exception: it writes the verification URI and user code to stderr before polling so the user can approve the request. With `--json`, this is one `verification_required` progress object; on denial or failure, one stable error envelope follows it and stdout remains empty. On success, stdout still contains exactly one final JSON value. Human mode writes two concise instruction lines to stderr and the final result to stdout. No browser is opened automatically. `stats` calls only modern MCP `get_community_stats`. `member` is an exact name-or-slug lookup through `lookup_member`; it never enumerates the directory or falls back to content or map search. `verify` calls only `verify_certificate`. `content list` and `content search` use `/api/v1/content`; an empty page is successful. `docs ask` posts a non-streaming request directly to `/ask`. `batch` accepts a strict JSON file or `-` for stdin, caps input at 262,144 bytes before parsing, and locally permits only `content.list` and `docs.ask` with their closed argument schemas. Unknown/member/registration operations and URL/header/credential-bearing argument escapes are rejected before network access; responses must preserve each item's ordered ID and operation. +## User-claimed authorization + +`auth login` requires `--login-hint ` and optionally accepts either or both PAGE scopes, repeated with `--scope`: `agent.account.read` and `agent.registrations.read`. The CLI discovers authorization from the unauthenticated `/api` challenge through the exact path-scoped RFC 9728 metadata and RFC 8414 authorization-server metadata before sending any claim or bearer value. It implements the WorkOS `service_auth` claim ceremony with a 15-minute local deadline, the advertised polling interval, and cumulative five-second `slow_down` increases. The login hint is sent only in the strict identity request and is not persisted. + +`auth status` is a live own-account request. When the access token has expired and the identity assertion is still valid, the CLI uses the discovered RFC 7523 JWT-bearer exchange, atomically replaces the access token only after full response validation, and then calls the own-account endpoint with exactly one Authorization header. A 401/invalid grant reports unauthenticated without destroying recoverable state; insufficient scope is distinct. + +`auth logout` removes local credential state only and makes no remote request. `auth revoke` submits the current access token to the discovered RFC 7009 endpoint and removes matching local state only after HTTP 200. RFC 7009 HTTP 200 means the server accepted processing even when a token was unknown. This command does not revoke the stored identity assertion or cancel the member's delegation; delegation management remains in the PAGE members UI. Non-200 responses and timeouts preserve local state because the token may still work. + +Credentials are supported only on macOS and Linux and are stored at: + +- macOS: `~/Library/Application Support/agentcommunity/credentials.json` +- Linux: `${XDG_CONFIG_HOME:-~/.config}/agentcommunity/credentials.json` + +The store requires a user-owned, non-symlink `0700` directory and a user-owned regular single-link `0600` file. Writers use a bounded exclusive lock, a same-directory `O_CREAT|O_EXCL|O_NOFOLLOW` `0600` temporary file, complete write and fsync, atomic rename, and directory fsync. Unsafe owners, modes, symlinks, hardlinks, path roots, locks, and interrupted replacements fail closed. Claim tokens, claim-attempt tokens, user codes, verification URIs, and login hints are never persisted. + For write-capable certificate registration use [@agentcommunity/dmv-agent](https://www.npmjs.com/package/@agentcommunity/dmv-agent). For AID diagnostics use [@agentcommunity/aid-doctor](https://www.npmjs.com/package/@agentcommunity/aid-doctor). Their behavior is intentionally not copied into this umbrella CLI. ## Exit codes @@ -41,7 +62,7 @@ For write-capable certificate registration use [@agentcommunity/dmv-agent](https | 0 | Success, including an empty content page, or all batch items succeeded | | 2 | Usage/local input error or invalid certificate format | | 3 | Member not found/ambiguous or certificate not issued | -| 4 | Reserved for Task 6.2 auth and credential safety | +| 4 | Authorization denied/missing/expired, insufficient scope, or credential-store safety failure | | 5 | Remote protocol, schema, or pinned-contract mismatch | | 6 | Timeout, network failure, upstream unavailable, or certificate verifier unavailable | | 7 | Rate limited; a valid bounded `Retry-After` value is included when available | @@ -49,7 +70,7 @@ For write-capable certificate registration use [@agentcommunity/dmv-agent](https ## Privacy and network behavior -There is no telemetry, analytics identifier, update ping, request-body logging, credential logging, or hidden network request. Production requests are fixed to `https://agentcommunity.org`; there is no runtime `--base-url`. Redirects are rejected, response sizes are capped, JSON MIME and schemas are validated, and ordinary commands are never retried automatically. +There is no telemetry, analytics identifier, update ping, request-body logging, credential logging, or hidden network request. Production requests are fixed to `https://agentcommunity.org`; there is no runtime `--base-url`. Redirects are rejected, response sizes are capped, JSON MIME and schemas are validated, and ordinary commands are never retried automatically. Auth polling alone repeats according to the discovered ceremony contract and never resets its local deadline after a network interruption. Token-like values are redacted from error serialization; access tokens travel only in the Authorization header or the exact RFC 7009 form, and assertions/claim tokens travel only in their standard discovered endpoint forms. Runtime commands use the committed PAGE contract bundle `1.0.0`, whose manifest SHA-256 is `b1f10b6288e436ccdca282b88a9a9115fcc0f6716f90731aab1455175b535595`. Contract sync is an explicit maintainer operation and never runs during install or normal execution. @@ -66,4 +87,4 @@ npm run contracts:check npm run package:audit ``` -Publishing, release automation, production deployment, and PAGE linking require separate owner authorization and live verification. +Publishing, release automation, production deployment, and PAGE linking require separate owner authorization and live verification. PAGE agent authorization is not live as of this source change. Do not run a real login, status, or revoke until PAGE auth is deployed and an owner explicitly authorizes a dedicated test account. diff --git a/SECURITY.md b/SECURITY.md index 870cd67..5a46e5e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,4 +4,8 @@ Report suspected vulnerabilities privately to `security@agentcommunity.org`. Do The supported source targets are the maintained Node.js versions declared in `package.json` on macOS and Linux. No npm release has been published yet, so there is no released version support table. -The CLI sends only explicitly requested read-only operations to fixed `https://agentcommunity.org` endpoints. It has no telemetry, update checks, credential collection, or install-time network script. Redirects are rejected and network errors are sanitized. +The CLI sends only explicitly requested public-data or user-claimed authorization operations to fixed `https://agentcommunity.org` endpoints. It has no telemetry, update checks, install-time network script, silent browser opening, or runtime base-URL override. Redirects are rejected, discovery and response contracts fail closed, and network errors are sanitized. + +User authorization is limited to read-only own-account scopes. The CLI validates path-scoped protected-resource and authorization-server metadata before sending any bearer, assertion, or claim value. Access tokens are used only in Authorization headers and the RFC 7009 revocation form; assertions and claim tokens are sent only in the exact discovered standard forms. Claim tokens, claim-attempt tokens, verification URIs, user codes, and login hints are never persisted. `auth revoke` processes only the current access token and does not cancel a member delegation. + +On macOS/Linux, credential storage requires user-owned non-symlink `0700` directories and user-owned regular single-link `0600` files. Writes use bounded exclusive locking, exclusive/no-follow same-directory temporary files, fsync, atomic rename, directory fsync, and conditional replacement/removal. Unsafe paths, ownership, permissions, links, locks, or interrupted writes fail closed. Windows is not supported. diff --git a/package.json b/package.json index 730b617..57a2dca 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@agentcommunity/cli", "version": "0.1.0", - "description": "Official read-only command-line client for Agent Community", + "description": "Official command-line client for Agent Community public data and user-claimed authorization", "type": "module", "bin": { "agentcommunity": "./dist/cli.js" diff --git a/scripts/audit-package.ts b/scripts/audit-package.ts index 14849bf..9809f91 100644 --- a/scripts/audit-package.ts +++ b/scripts/audit-package.ts @@ -55,8 +55,13 @@ async function main(): Promise { command("npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund", tarballPath], project); const smoke = spawnSync("npx", ["--no-install", "agentcommunity", "--help"], { cwd: project, encoding: "utf8", maxBuffer: 1024 * 1024 }); const help = `${smoke.stdout}${smoke.stderr}`; - if (smoke.status !== 0 || !help.includes("Agent Community read-only CLI") || !help.includes("agentcommunity batch ")) throw new Error(`Clean-install binary help smoke failed. Output:\n${help}`); - process.stdout.write(`${JSON.stringify({ filename: result.filename, sha256: tarballSha256, files: result.files, clean_install_help: "passed" }, null, 2)}\n`); + if (smoke.status !== 0 || !help.includes("Agent Community CLI") || !help.includes("agentcommunity batch ") || !help.includes("agentcommunity auth ")) throw new Error(`Clean-install binary help smoke failed. Output:\n${help}`); + const authSmoke = spawnSync("npx", ["--no-install", "agentcommunity", "auth", "--help"], { cwd: project, encoding: "utf8", maxBuffer: 1024 * 1024 }); + const authHelp = `${authSmoke.stdout}${authSmoke.stderr}`; + if (authSmoke.status !== 0 || !authHelp.includes("auth login --login-hint ") || !authHelp.includes("auth revoke")) { + throw new Error(`Clean-install binary auth-help smoke failed. Output:\n${authHelp}`); + } + process.stdout.write(`${JSON.stringify({ filename: result.filename, sha256: tarballSha256, files: result.files, clean_install_help: "passed", clean_install_auth_help: "passed" }, null, 2)}\n`); } finally { await rm(destination, { recursive: true, force: true }); await rm(project, { recursive: true, force: true }); diff --git a/src/__tests__/commands.test.ts b/src/__tests__/commands.test.ts index 48144a9..ed610a2 100644 --- a/src/__tests__/commands.test.ts +++ b/src/__tests__/commands.test.ts @@ -7,7 +7,12 @@ function harness(overrides: Partial = {}) { let stderr = ""; const dependencies: CliDependencies = { http: { requestJson: vi.fn() }, + authHttp: { requestAuth: vi.fn() }, mcp: { callTool: vi.fn() }, + credentials: { read: vi.fn(), write: vi.fn(), replace: vi.fn(), remove: vi.fn() }, + monotonicNow: () => 0, + wallNow: () => 0, + sleep: vi.fn(), readFile: vi.fn(), readStdin: vi.fn(), stdout: (value) => { stdout += value; }, @@ -157,7 +162,7 @@ describe("the seven read-only commands", () => { test("help and local usage have no hidden network or telemetry request", async () => { const helpHarness = harness(); expect(await runCli(["--help"], helpHarness.dependencies)).toBe(0); - expect(helpHarness.output().stdout).toContain("Agent Community read-only CLI"); + expect(helpHarness.output().stdout).toContain("Agent Community CLI"); expect(helpHarness.dependencies.http.requestJson).not.toHaveBeenCalled(); expect(helpHarness.dependencies.mcp.callTool).not.toHaveBeenCalled(); }); diff --git a/src/__tests__/http.test.ts b/src/__tests__/http.test.ts index ea718e3..eb77bea 100644 --- a/src/__tests__/http.test.ts +++ b/src/__tests__/http.test.ts @@ -7,6 +7,37 @@ function response(body: string, init: ResponseInit = {}) { } describe("bounded HTTP transport", () => { + test("auth transport accepts only fixed-origin HTTPS URLs and returns bounded raw statuses", async () => { + const fetchImpl = vi.fn().mockResolvedValue(new Response("challenge", { + status: 401, + headers: { "WWW-Authenticate": "Bearer resource_metadata=\"fixture\"", "X-Ignored": "value" }, + })); + const client = new HttpClient(fetchImpl); + const result = await client.requestAuth({ + method: "GET", + url: "https://agentcommunity.org/api", + timeoutMs: 1_000, + maxBytes: 100, + headers: { Accept: "application/json" }, + }); + expect(result).toEqual({ + status: 401, + headers: { + "content-type": "text/plain;charset=UTF-8", + "www-authenticate": "Bearer resource_metadata=\"fixture\"", + "x-ignored": "value", + }, + body: new Uint8Array(Buffer.from("challenge")), + }); + expect(fetchImpl).toHaveBeenCalledWith("https://agentcommunity.org/api", expect.objectContaining({ redirect: "manual", method: "GET" })); + + for (const url of ["http://agentcommunity.org/api", "https://evil.example/api", "https://user@agentcommunity.org/api"] ) { + await expect(client.requestAuth({ method: "GET", url, timeoutMs: 1_000, maxBytes: 100, headers: {} })) + .rejects.toMatchObject({ exitCode: 5, code: "unsafe_auth_endpoint" }); + } + expect(fetchImpl).toHaveBeenCalledTimes(1); + }); + test("uses the fixed production origin, manual redirects, JSON headers, and no extra request", async () => { const fetchImpl = vi.fn().mockResolvedValue(response('{"ok":true}')); const client = new HttpClient(fetchImpl); diff --git a/src/__tests__/package-boundary.test.ts b/src/__tests__/package-boundary.test.ts index 3cb77a0..8b2930b 100644 --- a/src/__tests__/package-boundary.test.ts +++ b/src/__tests__/package-boundary.test.ts @@ -29,4 +29,10 @@ describe("package and CI boundaries", () => { expect(workflow).not.toContain("id-token: write"); await expect(readFile(new URL(".github/workflows/release.yml", root), "utf8")).rejects.toThrow(); }); + + test("the packed-tarball audit smoke checks both root and auth help", async () => { + const audit = await readFile(new URL("scripts/audit-package.ts", root), "utf8"); + expect(audit).toContain('["--no-install", "agentcommunity", "--help"]'); + expect(audit).toContain('["--no-install", "agentcommunity", "auth", "--help"]'); + }); }); diff --git a/src/auth/__tests__/auth-commands.test.ts b/src/auth/__tests__/auth-commands.test.ts new file mode 100644 index 0000000..87fda3d --- /dev/null +++ b/src/auth/__tests__/auth-commands.test.ts @@ -0,0 +1,191 @@ +import { describe, expect, test, vi } from "vitest"; + +import type { AuthHttpRequest, AuthHttpResponse, AuthHttpTransport } from "../../http.js"; +import { runAuthLogout, runAuthRevoke, runAuthStatus, type CredentialStore } from "../../commands/auth.js"; +import { AUTH_SCOPES, JWT_BEARER_GRANT, PROTECTED_RESOURCE_METADATA_URL } from "../discovery.js"; +import type { CredentialRecord } from "../device-flow.js"; + +const credential: CredentialRecord = { + format_version: 1, + bundle_version: "1.0.0", + issuer: "https://agentcommunity.org", + resource: "https://agentcommunity.org/api", + scopes: [...AUTH_SCOPES], + access_token: "current-access-token-fixture-only", + access_token_expires_at: "2026-08-02T01:00:00.000Z", + identity_assertion: "identity-assertion-fixture-only", + assertion_expires_at: "2099-01-01T00:00:00.000Z", +}; + +const prm = { + authorization_servers: ["https://agentcommunity.org"], bearer_methods_supported: ["header"], + resource: "https://agentcommunity.org/api", resource_documentation: "https://agentcommunity.org/auth.md", + resource_name: "Agent Community agent API", resource_policy_uri: "https://agentcommunity.org/terms", + scopes_supported: [...AUTH_SCOPES], +}; +const asMetadata = { + agent_auth: { + claim_endpoint: "https://agentcommunity.org/agent/identity/claim", + identity_endpoint: "https://agentcommunity.org/agent/identity", + identity_types_supported: ["service_auth"], skill: "https://agentcommunity.org/auth.md", + }, + grant_types_supported: [JWT_BEARER_GRANT, "urn:workos:agent-auth:grant-type:claim"], + issuer: "https://agentcommunity.org", jwks_uri: "https://agentcommunity.org/.well-known/jwks.json", + protected_resources: ["https://agentcommunity.org/api"], + revocation_endpoint: "https://agentcommunity.org/oauth2/revoke", revocation_endpoint_auth_methods_supported: ["none"], + scopes_supported: [...AUTH_SCOPES], token_endpoint: "https://agentcommunity.org/oauth2/token", + token_endpoint_auth_methods_supported: ["none"], +}; +const account = { + account: { email: "fixture@example.invalid", email_verified: true, id: "00000000-0000-4000-8000-000000000601" }, + authorization: { + access_token_expires_at: "2099-01-01T01:00:00.000Z", delegation_expires_at: "2099-01-02T00:00:00.000Z", + registration_id: "00000000-0000-4000-8000-000000000501", scopes: [...AUTH_SCOPES], status: "approved", + }, +}; + +function json(status: number, body: unknown): AuthHttpResponse { + return { status, headers: { "content-type": "application/json" }, body: Buffer.from(JSON.stringify(body)) }; +} + +function discoveryResponses(): Array { + return [ + { status: 401, headers: { "www-authenticate": `Bearer resource_metadata="${PROTECTED_RESOURCE_METADATA_URL}"` }, body: new Uint8Array() }, + json(200, prm), + json(200, asMetadata), + ]; +} + +function httpHarness(responses: Array) { + const requests: Array = []; + const http: AuthHttpTransport = { + requestAuth: vi.fn(async (request) => { + requests.push(request); + const response = responses.shift(); + if (response instanceof Error) throw response; + if (response === undefined) throw new Error("unexpected request"); + return response; + }), + }; + return { http, requests }; +} + +function storeHarness(value: CredentialRecord | null = credential) { + let current = value; + const writes: Array = []; + const store: CredentialStore = { + read: vi.fn(async () => current), + write: vi.fn(async (next) => { writes.push(next); current = next; }), + replace: vi.fn(async (expected, next) => { + if (current === null || JSON.stringify(expected) !== JSON.stringify(current)) return false; + writes.push(next); + current = next; + return true; + }), + remove: vi.fn(async (expected) => { + if (current === null) return false; + if (expected !== undefined && JSON.stringify(expected) !== JSON.stringify(current)) return false; + current = null; + return true; + }), + }; + return { store, writes, current: () => current }; +} + +describe("auth status, logout, and RFC 7009 revoke", () => { + test("status performs a live own-account request with exactly one bearer header", async () => { + const network = httpHarness([...discoveryResponses(), json(200, account)]); + const local = storeHarness(); + const result = await runAuthStatus({ http: network.http, store: local.store, timeoutMs: 10_000, wallNow: () => Date.parse("2026-08-02T00:00:00.000Z") }); + expect(result).toMatchObject({ exitCode: 0, payload: { authenticated: true, ...account } }); + const accountRequest = network.requests.at(-1); + expect(accountRequest?.url).toBe("https://agentcommunity.org/api/v1/agent/account"); + expect(accountRequest?.headers).toEqual({ Accept: "application/json", Authorization: `Bearer ${credential.access_token}` }); + expect(Object.keys(accountRequest?.headers ?? {}).filter((name) => name.toLowerCase() === "authorization")).toHaveLength(1); + expect(local.writes).toEqual([]); + }); + + test("status refreshes an expired access token via exact JWT bearer form before the live call", async () => { + const expired = { ...credential, access_token_expires_at: "2026-08-01T00:00:00.000Z" }; + const refreshed = { access_token: "refreshed-access-token-fixture-only", token_type: "Bearer", expires_in: 3600, scope: AUTH_SCOPES.join(" ") }; + const network = httpHarness([...discoveryResponses(), json(200, refreshed), json(200, account)]); + const local = storeHarness(expired); + const result = await runAuthStatus({ http: network.http, store: local.store, timeoutMs: 10_000, wallNow: () => Date.parse("2026-08-02T00:00:00.000Z") }); + expect(result.exitCode).toBe(0); + const refreshRequest = network.requests[3]; + expect(refreshRequest).toMatchObject({ url: "https://agentcommunity.org/oauth2/token", method: "POST" }); + expect(new URLSearchParams(refreshRequest?.body)).toEqual(new URLSearchParams({ + grant_type: JWT_BEARER_GRANT, + assertion: expired.identity_assertion, + resource: expired.resource, + })); + expect(local.writes).toEqual([{ ...expired, access_token: refreshed.access_token, access_token_expires_at: "2026-08-02T01:00:00.000Z" }]); + expect(network.requests[4]?.headers.Authorization).toBe(`Bearer ${refreshed.access_token}`); + }); + + test.each([ + [401, { error: "unauthorized" }, "unauthenticated"], + [403, { error: "forbidden" }, "insufficient_scope"], + ])("status distinguishes HTTP %s without removing recoverable state", async (status, body, reason) => { + const network = httpHarness([...discoveryResponses(), json(status, body)]); + const local = storeHarness(); + const result = await runAuthStatus({ http: network.http, store: local.store, timeoutMs: 10_000, wallNow: () => Date.parse("2026-08-02T00:00:00.000Z") }); + expect(result).toMatchObject({ exitCode: 4, payload: { authenticated: false, reason } }); + expect(local.store.remove).not.toHaveBeenCalled(); + expect(local.current()).toEqual(credential); + }); + + test("invalid-grant refresh is unauthenticated and preserves the assertion", async () => { + const expired = { ...credential, access_token_expires_at: "2026-08-01T00:00:00.000Z" }; + const network = httpHarness([...discoveryResponses(), json(400, { error: "invalid_grant", error_description: "The authorization grant is invalid" })]); + const local = storeHarness(expired); + const result = await runAuthStatus({ http: network.http, store: local.store, timeoutMs: 10_000, wallNow: () => Date.parse("2026-08-02T00:00:00.000Z") }); + expect(result).toMatchObject({ exitCode: 4, payload: { authenticated: false, reason: "unauthenticated" } }); + expect(local.current()).toEqual(expired); + expect(local.writes).toEqual([]); + }); + + test("status maps upstream outage and response mismatch without exposing credentials", async () => { + const local = storeHarness(); + await expect(runAuthStatus({ http: httpHarness([...discoveryResponses(), json(503, { error: "service_unavailable" })]).http, store: local.store, timeoutMs: 10_000, wallNow: Date.now })) + .rejects.toMatchObject({ exitCode: 6 }); + await expect(runAuthStatus({ http: httpHarness([...discoveryResponses(), json(200, { ...account, extra: credential.access_token })]).http, store: local.store, timeoutMs: 10_000, wallNow: Date.now })) + .rejects.toMatchObject({ exitCode: 5 }); + const reversedScopes = { ...account, authorization: { ...account.authorization, scopes: [...AUTH_SCOPES].reverse() } }; + await expect(runAuthStatus({ http: httpHarness([...discoveryResponses(), json(200, reversedScopes)]).http, store: local.store, timeoutMs: 10_000, wallNow: Date.now })) + .rejects.toMatchObject({ exitCode: 5 }); + }); + + test("logout makes zero remote calls and is safely idempotent", async () => { + const local = storeHarness(); + expect(await runAuthLogout(local.store)).toMatchObject({ exitCode: 0, payload: { logged_out: true, credential_removed: true } }); + expect(await runAuthLogout(local.store)).toMatchObject({ exitCode: 0, payload: { logged_out: true, credential_removed: false } }); + }); + + test("revoke sends only the current access token in the RFC 7009 form then removes matching local state on HTTP 200", async () => { + const network = httpHarness([...discoveryResponses(), { status: 200, headers: {}, body: new Uint8Array() }]); + const local = storeHarness(); + const result = await runAuthRevoke({ http: network.http, store: local.store, timeoutMs: 10_000 }); + expect(result).toMatchObject({ exitCode: 0, payload: { revoked: true, credential_removed: true } }); + const revoke = network.requests.at(-1); + expect(revoke).toMatchObject({ + method: "POST", url: "https://agentcommunity.org/oauth2/revoke", + headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, + }); + expect(new URLSearchParams(revoke?.body)).toEqual(new URLSearchParams({ token: credential.access_token, token_type_hint: "access_token" })); + expect(revoke?.body).not.toContain(credential.identity_assertion); + expect(local.store.remove).toHaveBeenCalledWith(credential); + }); + + test.each([ + [json(503, { error: "temporarily_unavailable" }), 6], + [json(400, { error: "invalid_request", error_description: "The request is malformed" }), 5], + [new Error("timeout containing current-access-token-fixture-only"), 6], + ])("revoke preserves local state when remote acceptance is not HTTP 200", async (failure, exitCode) => { + const network = httpHarness([...discoveryResponses(), failure]); + const local = storeHarness(); + await expect(runAuthRevoke({ http: network.http, store: local.store, timeoutMs: 10_000 })).rejects.toMatchObject({ exitCode }); + expect(local.current()).toEqual(credential); + expect(local.store.remove).not.toHaveBeenCalled(); + }); +}); diff --git a/src/auth/__tests__/cli-auth.test.ts b/src/auth/__tests__/cli-auth.test.ts new file mode 100644 index 0000000..19822b0 --- /dev/null +++ b/src/auth/__tests__/cli-auth.test.ts @@ -0,0 +1,184 @@ +import { describe, expect, test, vi } from "vitest"; + +import { runCli, type CliDependencies } from "../../cli.js"; +import type { CredentialStore } from "../../commands/auth.js"; +import { CliError } from "../../errors.js"; +import type { AuthHttpRequest, AuthHttpResponse } from "../../http.js"; +import { AUTH_SCOPES, PROTECTED_RESOURCE_METADATA_URL } from "../discovery.js"; +import type { CredentialRecord } from "../device-flow.js"; + +const prm = { + authorization_servers: ["https://agentcommunity.org"], bearer_methods_supported: ["header"], + resource: "https://agentcommunity.org/api", resource_documentation: "https://agentcommunity.org/auth.md", + resource_name: "Agent Community agent API", resource_policy_uri: "https://agentcommunity.org/terms", + scopes_supported: [...AUTH_SCOPES], +}; +const asMetadata = { + agent_auth: { claim_endpoint: "https://agentcommunity.org/agent/identity/claim", identity_endpoint: "https://agentcommunity.org/agent/identity", identity_types_supported: ["service_auth"], skill: "https://agentcommunity.org/auth.md" }, + grant_types_supported: ["urn:ietf:params:oauth:grant-type:jwt-bearer", "urn:workos:agent-auth:grant-type:claim"], + issuer: "https://agentcommunity.org", jwks_uri: "https://agentcommunity.org/.well-known/jwks.json", + protected_resources: ["https://agentcommunity.org/api"], revocation_endpoint: "https://agentcommunity.org/oauth2/revoke", + revocation_endpoint_auth_methods_supported: ["none"], scopes_supported: [...AUTH_SCOPES], + token_endpoint: "https://agentcommunity.org/oauth2/token", token_endpoint_auth_methods_supported: ["none"], +}; +const start = { + registration_id: "00000000-0000-4000-8000-000000000501", registration_type: "service_auth", + claim_url: "https://agentcommunity.org/agent/identity/claim", claim_token: "claim-token-fixture-only", + claim_token_expires: "2099-01-01T00:00:00.000Z", post_claim_scopes: [...AUTH_SCOPES], + claim: { user_code: "000000", expires_in: 600, verification_uri: "https://agentcommunity.org/agent/authorize?claim_attempt_token=claim-attempt-fixture-only", interval: 5 }, +}; +const success = { + access_token: "access-token-fixture-only", token_type: "Bearer", expires_in: 3600, + scope: AUTH_SCOPES.join(" "), identity_assertion: "identity-assertion-fixture-only", assertion_expires: "2099-01-01T00:00:00.000Z", +}; + +function json(status: number, body: unknown): AuthHttpResponse { + return { status, headers: { "content-type": "application/json" }, body: Buffer.from(JSON.stringify(body)) }; +} + +function discovery(): Array { + return [ + { status: 401, headers: { "www-authenticate": `Bearer resource_metadata="${PROTECTED_RESOURCE_METADATA_URL}"` }, body: new Uint8Array() }, + json(200, prm), json(200, asMetadata), + ]; +} + +function harness(responses: Array = []) { + let stdout = ""; + let stderr = ""; + const requests: Array = []; + const writes: Array = []; + let current: CredentialRecord | null = null; + const credentials: CredentialStore = { + read: vi.fn(async () => current), + write: vi.fn(async (value) => { current = value; writes.push(value); }), + replace: vi.fn(async (expected, value) => { + if (JSON.stringify(current) !== JSON.stringify(expected)) return false; + current = value; + writes.push(value); + return true; + }), + remove: vi.fn(async () => { const removed = current !== null; current = null; return removed; }), + }; + let monotonic = 0; + const dependencies: CliDependencies = { + http: { requestJson: vi.fn() }, + authHttp: { + requestAuth: vi.fn(async (request) => { + requests.push(request); + const response = responses.shift(); + if (response instanceof Error) throw response; + if (response === undefined) throw new Error("unexpected request"); + return response; + }), + }, + mcp: { callTool: vi.fn() }, + credentials, + monotonicNow: () => monotonic, + wallNow: () => Date.parse("2026-08-02T00:00:00.000Z"), + sleep: async (milliseconds) => { monotonic += milliseconds; }, + readFile: vi.fn(), readStdin: vi.fn(), + stdout: (value) => { stdout += value; }, stderr: (value) => { stderr += value; }, + }; + return { dependencies, credentials, requests, writes, output: () => ({ stdout, stderr }) }; +} + +describe("auth CLI integration", () => { + test("auth help is local and shows the required login hint and all four commands", async () => { + const cli = harness(); + expect(await runCli(["auth", "--help"], cli.dependencies)).toBe(0); + expect(cli.output().stdout).toContain("auth login --login-hint "); + expect(cli.output().stdout).toContain("auth status"); + expect(cli.output().stdout).toContain("auth logout"); + expect(cli.output().stdout).toContain("auth revoke"); + expect(cli.requests).toEqual([]); + }); + + test("requires login hint and rejects unsupported or duplicate scopes before discovery", async () => { + for (const args of [ + ["auth", "login"], + ["auth", "login", "--login-hint", "invalid"], + ["auth", "login", "--login-hint", "fixture@example.invalid", "--scope", "admin"], + ["auth", "login", "--login-hint", "fixture@example.invalid", "--scope", "agent.account.read", "--scope", "agent.account.read"], + ]) { + const cli = harness(); + expect(await runCli(args, cli.dependencies)).toBe(2); + expect(cli.requests).toEqual([]); + expect(cli.output().stdout).toBe(""); + } + }); + + test("normalizes repeated scope options to PAGE order and keeps JSON stdout singular", async () => { + const cli = harness([...discovery(), json(200, start), json(200, success)]); + expect(await runCli([ + "auth", "login", "--login-hint", " Fixture@Example.invalid ", + "--scope", "agent.registrations.read", "--scope", "agent.account.read", "--json", + ], cli.dependencies)).toBe(0); + const identityRequest = cli.requests[3]; + expect(JSON.parse(identityRequest?.body ?? "null")).toMatchObject({ + login_hint: "Fixture@Example.invalid", + scopes: [...AUTH_SCOPES], + }); + expect(cli.output().stderr).toBe(`${JSON.stringify({ + event: "verification_required", + verification_uri: start.claim.verification_uri, + user_code: start.claim.user_code, + })}\n`); + const final = JSON.parse(cli.output().stdout); + expect(final).toMatchObject({ authenticated: true, scopes: [...AUTH_SCOPES] }); + expect(cli.output().stdout.trim().split("\n")).toHaveLength(1); + expect(cli.output().stdout).not.toContain(success.access_token); + expect(cli.output().stdout).not.toContain(success.identity_assertion); + expect(JSON.stringify(cli.writes)).not.toContain(start.claim_token); + expect(JSON.stringify(cli.writes)).not.toContain(start.claim.verification_uri); + }); + + test("prints ordered human verification progress before the final success", async () => { + const cli = harness([...discovery(), json(200, start), json(200, success)]); + expect(await runCli(["auth", "login", "--login-hint", "fixture@example.invalid"], cli.dependencies)).toBe(0); + expect(cli.output().stderr).toBe(`Open ${start.claim.verification_uri}\nEnter code ${start.claim.user_code}\n`); + expect(cli.output().stdout).toContain("Authorization complete."); + }); + + test("on denial, progress precedes one stable error envelope and stdout remains empty", async () => { + const denied = { error: "access_denied", error_description: "Authorization was denied" }; + const cli = harness([...discovery(), json(200, start), json(400, denied)]); + expect(await runCli(["auth", "login", "--login-hint", "fixture@example.invalid", "--json"], cli.dependencies)).toBe(4); + expect(cli.output().stdout).toBe(""); + const lines = cli.output().stderr.trim().split("\n"); + expect(JSON.parse(lines[0] ?? "null")).toMatchObject({ event: "verification_required", user_code: "000000" }); + expect(JSON.parse(lines[1] ?? "null")).toEqual({ error: { code: "authorization_denied", message: "Authorization was denied." } }); + expect(lines).toHaveLength(2); + expect(cli.writes).toEqual([]); + }); + + test("logout is local-only through the CLI and revoke copy never claims delegation cancellation", async () => { + const cli = harness(); + expect(await runCli(["auth", "logout", "--json"], cli.dependencies)).toBe(0); + expect(cli.requests).toEqual([]); + expect(JSON.parse(cli.output().stdout)).toMatchObject({ logged_out: true }); + + const invalidTimeout = harness(); + expect(await runCli(["auth", "logout", "--timeout", "1000"], invalidTimeout.dependencies)).toBe(2); + expect(invalidTimeout.credentials.remove).not.toHaveBeenCalled(); + + const help = harness(); + await runCli(["auth", "--help"], help.dependencies); + expect(help.output().stdout.toLowerCase()).not.toContain("delegation"); + expect(help.output().stdout.toLowerCase()).not.toContain("cancel"); + }); + + test("redacts token, assertion, code, and verification values from error envelopes", async () => { + const secrets = ["aca_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "identity-assertion-fixture-only", "123456", "https://agentcommunity.org/agent/authorize?claim_attempt_token=secret"]; + for (const secret of secrets) { + const cli = harness(); + cli.dependencies.credentials.read = vi.fn(async () => { + throw new CliError("credential_error", `unsafe ${secret}`, 4, { token: secret, nested: { assertion: secret } }); + }); + expect(await runCli(["auth", "status", "--json"], cli.dependencies)).toBe(4); + expect(cli.output().stdout).toBe(""); + expect(cli.output().stderr).not.toContain(secret); + expect(JSON.parse(cli.output().stderr)).toMatchObject({ error: { code: "credential_error" } }); + } + }); +}); diff --git a/src/auth/__tests__/credential-store.test.ts b/src/auth/__tests__/credential-store.test.ts new file mode 100644 index 0000000..e8a045c --- /dev/null +++ b/src/auth/__tests__/credential-store.test.ts @@ -0,0 +1,243 @@ +import * as fs from "node:fs/promises"; +import { constants } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, describe, expect, test, vi } from "vitest"; + +import { + PosixCredentialStore, + credentialPath, + type CredentialFileSystem, +} from "../credential-store.js"; +import type { CredentialRecord } from "../device-flow.js"; + +const uid = process.getuid?.() ?? 0; +const credential: CredentialRecord = { + format_version: 1, + bundle_version: "1.0.0", + issuer: "https://agentcommunity.org", + resource: "https://agentcommunity.org/api", + scopes: ["agent.account.read", "agent.registrations.read"], + access_token: "access-token-fixture-only", + access_token_expires_at: "2099-01-01T01:00:00.000Z", + identity_assertion: "identity-assertion-fixture-only", + assertion_expires_at: "2099-01-01T00:00:00.000Z", +}; + +const roots: Array = []; + +async function root(): Promise { + const value = await fs.mkdtemp(join(tmpdir(), "agentcommunity-store-test-")); + roots.push(value); + return value; +} + +afterEach(async () => { + await Promise.all(roots.splice(0).map((value) => fs.rm(value, { recursive: true, force: true }))); +}); + +function store(homeDirectory: string, overrides: Partial[0]> = {}) { + let monotonic = 0; + return new PosixCredentialStore({ + platform: "linux", + homeDirectory, + uid, + processId: 4242, + randomId: () => "fixed-random", + monotonicNow: () => monotonic, + sleep: async (milliseconds) => { monotonic += milliseconds; }, + isProcessAlive: () => true, + lockTimeoutMs: 100, + staleLockMs: 60_000, + ...overrides, + }); +} + +describe("POSIX credential path and safe storage", () => { + test("selects exact macOS and Linux paths and rejects unsupported or relative roots", () => { + expect(credentialPath({ platform: "darwin", homeDirectory: "/Users/fixture" })) + .toBe("/Users/fixture/Library/Application Support/agentcommunity/credentials.json"); + expect(credentialPath({ platform: "linux", homeDirectory: "/home/fixture" })) + .toBe("/home/fixture/.config/agentcommunity/credentials.json"); + expect(credentialPath({ platform: "linux", homeDirectory: "/home/fixture", xdgConfigHome: "/safe/config" })) + .toBe("/safe/config/agentcommunity/credentials.json"); + expect(() => credentialPath({ platform: "win32", homeDirectory: "C:\\Users\\fixture" })).toThrow(expect.objectContaining({ exitCode: 4 })); + expect(() => credentialPath({ platform: "linux", homeDirectory: "relative/home" })).toThrow(expect.objectContaining({ exitCode: 4 })); + expect(() => credentialPath({ platform: "linux", homeDirectory: "/home/fixture", xdgConfigHome: "relative/config" })).toThrow(expect.objectContaining({ exitCode: 4 })); + }); + + test("creates a 0700 directory and atomically stores one 0600 regular single-link file", async () => { + const home = await root(); + const targetStore = store(home); + await targetStore.write(credential); + + const path = credentialPath({ platform: "linux", homeDirectory: home }); + const directoryStat = await fs.lstat(join(home, ".config", "agentcommunity")); + const fileStat = await fs.lstat(path); + expect(directoryStat.mode & 0o777).toBe(0o700); + expect(fileStat.mode & 0o777).toBe(0o600); + expect(fileStat.isFile()).toBe(true); + expect(fileStat.nlink).toBe(1); + expect(fileStat.uid).toBe(uid); + expect(await targetStore.read()).toEqual(credential); + expect(await fs.readdir(join(home, ".config", "agentcommunity"))).toEqual(["credentials.json"]); + }); + + test.each([ + ["symlink", async (path: string, home: string) => fs.symlink(join(home, "outside"), path)], + ["hardlink", async (path: string, home: string) => { + const outside = join(home, "outside"); + await fs.writeFile(outside, JSON.stringify(credential), { mode: 0o600 }); + await fs.link(outside, path); + }], + ["group-readable", async (path: string) => { + await fs.writeFile(path, JSON.stringify(credential), { mode: 0o640 }); + }], + ["directory", async (path: string) => fs.mkdir(path, { mode: 0o700 })], + ])("rejects an unsafe %s credential target", async (_label, createTarget) => { + const home = await root(); + const directory = join(home, ".config", "agentcommunity"); + await fs.mkdir(directory, { recursive: true, mode: 0o700 }); + const path = join(directory, "credentials.json"); + await createTarget(path, home); + await expect(store(home).read()).rejects.toMatchObject({ exitCode: 4, code: "unsafe_credential_store" }); + }); + + test("rejects unsafe directory modes and ownership expectations", async () => { + const home = await root(); + const directory = join(home, ".config", "agentcommunity"); + await fs.mkdir(directory, { recursive: true, mode: 0o755 }); + await expect(store(home).write(credential)).rejects.toMatchObject({ exitCode: 4, code: "unsafe_credential_store" }); + + await fs.chmod(directory, 0o700); + await expect(store(home, { uid: uid + 1 }).write(credential)).rejects.toMatchObject({ exitCode: 4, code: "unsafe_credential_store" }); + }); + + test("bounds lock contention and removes only a validated dead stale lock", async () => { + const home = await root(); + const targetStore = store(home); + await targetStore.write(credential); + const directory = join(home, ".config", "agentcommunity"); + const lockPath = join(directory, "credentials.json.lock"); + await fs.writeFile(lockPath, JSON.stringify({ pid: 9999, created_at_ms: 0 }), { mode: 0o600, flag: "wx" }); + + await expect(store(home).write({ ...credential, access_token: "replacement" })) + .rejects.toMatchObject({ exitCode: 4, code: "credential_store_locked" }); + expect(JSON.parse(await fs.readFile(lockPath, "utf8"))).toEqual({ pid: 9999, created_at_ms: 0 }); + + await fs.unlink(lockPath); + await fs.writeFile(lockPath, JSON.stringify({ pid: 9999, created_at_ms: 0 }), { mode: 0o600, flag: "wx" }); + const staleStore = store(home, { + monotonicNow: () => 120_000, + isProcessAlive: () => false, + }); + await staleStore.write({ ...credential, access_token: "replacement" }); + expect((await staleStore.read())?.access_token).toBe("replacement"); + await expect(fs.lstat(lockPath)).rejects.toMatchObject({ code: "ENOENT" }); + }); + + test("leaves a malformed stale lock in place conservatively", async () => { + const home = await root(); + const targetStore = store(home); + await targetStore.write(credential); + const lockPath = join(home, ".config", "agentcommunity", "credentials.json.lock"); + await fs.writeFile(lockPath, "not-json", { mode: 0o600, flag: "wx" }); + await expect(store(home, { isProcessAlive: () => false }).write(credential)) + .rejects.toMatchObject({ exitCode: 4, code: "credential_store_locked" }); + expect(await fs.readFile(lockPath, "utf8")).toBe("not-json"); + }); + + test("rejects symlink and hardlink lock files without removing them", async () => { + for (const kind of ["symlink", "hardlink"] as const) { + const home = await root(); + const targetStore = store(home); + await targetStore.write(credential); + const directory = join(home, ".config", "agentcommunity"); + const lockPath = join(directory, "credentials.json.lock"); + const outside = join(home, `${kind}-outside`); + await fs.writeFile(outside, JSON.stringify({ pid: 9999, created_at_ms: 0 }), { mode: 0o600 }); + if (kind === "symlink") await fs.symlink(outside, lockPath); + else await fs.link(outside, lockPath); + await expect(store(home).write(credential)).rejects.toMatchObject({ exitCode: 4, code: "unsafe_credential_store" }); + expect((await fs.lstat(lockPath)).isSymbolicLink()).toBe(kind === "symlink"); + } + }); + + test("cleans a task-created lock if lock initialization is interrupted", async () => { + const home = await root(); + const failingFs: CredentialFileSystem = { + ...fs, + open: async (path, flags, mode) => { + const handle = await fs.open(path, flags, mode); + if (String(path).endsWith("credentials.json.lock")) { + return new Proxy(handle, { + get(target, property, receiver) { + if (property === "writeFile") return async () => { throw Object.assign(new Error("interrupted lock write"), { code: "EIO" }); }; + const value = Reflect.get(target, property, receiver) as unknown; + return typeof value === "function" ? value.bind(target) : value; + }, + }); + } + return handle; + }, + }; + await expect(store(home, { fs: failingFs }).write(credential)) + .rejects.toMatchObject({ exitCode: 4, code: "credential_lock_failed" }); + expect(await fs.readdir(join(home, ".config", "agentcommunity"))).toEqual([]); + }); + + test("preserves the old credential and cleans its temp file when replacement fails before rename", async () => { + const home = await root(); + const targetStore = store(home); + await targetStore.write(credential); + const failingFs: CredentialFileSystem = { + ...fs, + rename: vi.fn(async () => { throw Object.assign(new Error("injected rename failure"), { code: "EIO" }); }), + }; + await expect(store(home, { fs: failingFs, randomId: () => "interrupted" }).write({ ...credential, access_token: "replacement" })) + .rejects.toMatchObject({ exitCode: 4, code: "credential_write_failed" }); + expect(await targetStore.read()).toEqual(credential); + expect(await fs.readdir(join(home, ".config", "agentcommunity"))).toEqual(["credentials.json"]); + }); + + test("logout is remote-free store removal, safe, and idempotent", async () => { + const home = await root(); + const targetStore = store(home); + expect(await targetStore.remove()).toBe(false); + await targetStore.write(credential); + expect(await targetStore.remove()).toBe(true); + expect(await targetStore.remove()).toBe(false); + expect(await targetStore.read()).toBeNull(); + }); + + test("conditional replacement preserves a credential changed by another writer", async () => { + const home = await root(); + const targetStore = store(home); + await targetStore.write(credential); + const changed = { ...credential, access_token: "other-writer" }; + await targetStore.write(changed); + expect(await targetStore.replace(credential, { ...credential, access_token: "stale-refresh" })).toBe(false); + expect(await targetStore.read()).toEqual(changed); + }); + + test("uses O_NOFOLLOW, O_EXCL and 0600 for lock and temporary files", async () => { + const home = await root(); + const opens: Array<{ path: string; flags: number; mode?: number }> = []; + const recordingFs: CredentialFileSystem = { + ...fs, + open: async (path, flags, mode) => { + if (typeof flags === "number") opens.push({ path: String(path), flags, ...(typeof mode === "number" ? { mode } : {}) }); + return fs.open(path, flags, mode); + }, + }; + await store(home, { fs: recordingFs }).write(credential); + const created = opens.filter((entry) => (entry.flags & constants.O_CREAT) !== 0); + expect(created).toHaveLength(2); + for (const entry of created) { + expect(entry.flags & constants.O_EXCL).not.toBe(0); + expect(entry.flags & constants.O_NOFOLLOW).not.toBe(0); + expect(entry.mode).toBe(0o600); + } + }); +}); diff --git a/src/auth/__tests__/device-flow.test.ts b/src/auth/__tests__/device-flow.test.ts new file mode 100644 index 0000000..bc584c8 --- /dev/null +++ b/src/auth/__tests__/device-flow.test.ts @@ -0,0 +1,207 @@ +import { describe, expect, test, vi } from "vitest"; + +import type { AuthHttpRequest, AuthHttpResponse, AuthHttpTransport } from "../../http.js"; +import { AUTH_SCOPES, CLAIM_GRANT, type AuthorizationDiscovery } from "../discovery.js"; +import { normalizeLoginHint, runServiceAuthLogin, type CredentialRecord } from "../device-flow.js"; + +const discovery: AuthorizationDiscovery = { + issuer: "https://agentcommunity.org", + resource: "https://agentcommunity.org/api", + scopes: [...AUTH_SCOPES], + identityEndpoint: "https://agentcommunity.org/agent/identity", + claimEndpoint: "https://agentcommunity.org/agent/identity/claim", + tokenEndpoint: "https://agentcommunity.org/oauth2/token", + revocationEndpoint: "https://agentcommunity.org/oauth2/revoke", +}; + +const claimToken = "claim-token-fixture-only"; +const accessToken = "access-token-fixture-only"; +const identityAssertion = "identity-assertion-fixture-only"; +const startBody = { + registration_id: "00000000-0000-4000-8000-000000000501", + registration_type: "service_auth", + claim_url: discovery.claimEndpoint, + claim_token: claimToken, + claim_token_expires: "2099-01-01T00:00:00.000Z", + post_claim_scopes: [...AUTH_SCOPES], + claim: { + user_code: "000000", + expires_in: 600, + verification_uri: "https://agentcommunity.org/agent/authorize?claim_attempt_token=claim-attempt-fixture-only", + interval: 5, + }, +}; +const successBody = { + access_token: accessToken, + token_type: "Bearer", + expires_in: 3600, + scope: AUTH_SCOPES.join(" "), + identity_assertion: identityAssertion, + assertion_expires: "2099-01-01T00:00:00.000Z", +}; + +function response(status: number, body: unknown): AuthHttpResponse { + return { status, headers: { "content-type": "application/json" }, body: Buffer.from(JSON.stringify(body)) }; +} + +function errorResponse(error: string, description: string): AuthHttpResponse { + return response(400, { error, error_description: description }); +} + +function harness(outcomes: Array, deadlineMs = 900_000) { + const requests: Array = []; + const events: Array = []; + const stored: Array = []; + const sleeps: Array = []; + let monotonic = 0; + const http: AuthHttpTransport = { + requestAuth: vi.fn(async (request) => { + requests.push(request); + events.push(request.url.endsWith("/agent/identity") ? "identity" : "poll"); + const outcome = outcomes.shift(); + if (outcome instanceof Error) throw outcome; + if (outcome === undefined) throw new Error("unexpected request"); + return outcome; + }), + }; + const promise = runServiceAuthLogin({ + http, + discovery, + loginHint: " Fixture@Example.invalid ", + requestedScopes: [...AUTH_SCOPES], + timeoutMs: 10_000, + deadlineMs, + monotonicNow: () => monotonic, + wallNow: () => Date.parse("2026-08-02T00:00:00.000Z"), + sleep: async (milliseconds) => { + sleeps.push(milliseconds); + events.push(`sleep:${milliseconds}`); + monotonic += milliseconds; + }, + presentVerification: (value) => { events.push(`present:${value.verificationUri}:${value.userCode}`); }, + store: async (value) => { stored.push(value); events.push("store"); }, + }); + return { promise, requests, events, sleeps, stored }; +} + +describe("WorkOS service_auth login", () => { + test("trims and validates the required login hint without lowercasing it", () => { + expect(normalizeLoginHint(" Fixture@Example.invalid ")).toBe("Fixture@Example.invalid"); + for (const value of ["", "not-an-email", "a@b", `${"a".repeat(310)}@example.com`]) { + expect(() => normalizeLoginHint(value)).toThrow(expect.objectContaining({ exitCode: 2 })); + } + }); + + test("prints verification details before polling and applies cumulative slow_down timing", async () => { + const flow = harness([ + response(200, startBody), + errorResponse("authorization_pending", "Authorization is still pending"), + errorResponse("slow_down", "Polling too quickly; increase the interval by 5 seconds"), + errorResponse("slow_down", "Polling too quickly; increase the interval by 5 seconds"), + response(200, successBody), + ]); + const credential = await flow.promise; + + expect(flow.events).toEqual([ + "identity", + `present:${startBody.claim.verification_uri}:000000`, + "sleep:5000", "poll", + "sleep:5000", "poll", + "sleep:10000", "poll", + "sleep:15000", "poll", + "store", + ]); + expect(flow.sleeps).toEqual([5_000, 5_000, 10_000, 15_000]); + expect(flow.requests[0]).toMatchObject({ + method: "POST", + url: discovery.identityEndpoint, + headers: { Accept: "application/json", "Content-Type": "application/json" }, + }); + expect(JSON.parse(flow.requests[0]?.body ?? "null")).toEqual({ + type: "service_auth", + login_hint: "Fixture@Example.invalid", + scopes: [...AUTH_SCOPES], + client_name: "@agentcommunity/cli", + }); + for (const request of flow.requests.slice(1)) { + expect(request.url).toBe(discovery.tokenEndpoint); + expect(request.headers).toEqual({ Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }); + expect(new URLSearchParams(request.body)).toEqual(new URLSearchParams({ grant_type: CLAIM_GRANT, claim_token: claimToken })); + } + expect(credential).toEqual(flow.stored[0]); + expect(credential).toMatchObject({ + format_version: 1, + bundle_version: "1.0.0", + issuer: discovery.issuer, + resource: discovery.resource, + scopes: [...AUTH_SCOPES], + access_token: accessToken, + access_token_expires_at: "2026-08-02T01:00:00.000Z", + identity_assertion: identityAssertion, + assertion_expires_at: successBody.assertion_expires, + }); + expect(JSON.stringify(credential)).not.toContain(claimToken); + expect(JSON.stringify(credential)).not.toContain("claim-attempt"); + expect(JSON.stringify(credential)).not.toContain("000000"); + expect(JSON.stringify(credential)).not.toContain("Fixture@Example.invalid"); + }); + + test.each([ + ["access_denied", "Authorization was denied", "authorization_denied"], + ["expired_token", "The claim token or current claim attempt has expired", "authorization_expired"], + ])("maps %s to an auth exit without storing", async (error, description, code) => { + const flow = harness([response(200, startBody), errorResponse(error, description)]); + await expect(flow.promise).rejects.toMatchObject({ exitCode: 4, code }); + expect(flow.stored).toEqual([]); + }); + + test("keeps the monotonic local deadline across network interruptions", async () => { + const flow = harness([response(200, startBody), new Error("network secret"), new Error("network secret")], 10_000); + await expect(flow.promise).rejects.toMatchObject({ exitCode: 6, code: "authorization_unavailable" }); + expect(flow.sleeps).toEqual([5_000, 5_000]); + expect(flow.stored).toEqual([]); + }); + + test("maps a pending ceremony's local deadline to the auth exit", async () => { + const flow = harness([ + response(200, startBody), + errorResponse("authorization_pending", "Authorization is still pending"), + ], 10_000); + await expect(flow.promise).rejects.toMatchObject({ exitCode: 4, code: "authorization_timeout" }); + expect(flow.sleeps).toEqual([5_000, 5_000]); + expect(flow.stored).toEqual([]); + }); + + test("rejects invalid deadline bounds before any request", async () => { + for (const deadlineMs of [0, 1_800_001]) { + const flow = harness([], deadlineMs); + await expect(flow.promise).rejects.toMatchObject({ exitCode: 2, code: "invalid_auth_deadline" }); + expect(flow.requests).toEqual([]); + } + }); + + test("validates the whole start and success responses before presentation or storage", async () => { + const invalidStart = harness([response(200, { ...startBody, claim_url: "https://evil.example/claim" })]); + await expect(invalidStart.promise).rejects.toMatchObject({ exitCode: 5 }); + expect(invalidStart.events).toEqual(["identity"]); + expect(invalidStart.stored).toEqual([]); + + const reversedStartScopes = harness([response(200, { ...startBody, post_claim_scopes: [...AUTH_SCOPES].reverse() })]); + await expect(reversedStartScopes.promise).rejects.toMatchObject({ exitCode: 5 }); + expect(reversedStartScopes.stored).toEqual([]); + + const invalidSuccess = harness([ + response(200, startBody), + response(200, { ...successBody, scope: "agent.account.read", extra: accessToken }), + ]); + await expect(invalidSuccess.promise).rejects.toMatchObject({ exitCode: 5 }); + expect(invalidSuccess.stored).toEqual([]); + + const reversedSuccessScopes = harness([ + response(200, startBody), + response(200, { ...successBody, scope: [...AUTH_SCOPES].reverse().join(" ") }), + ]); + await expect(reversedSuccessScopes.promise).rejects.toMatchObject({ exitCode: 5 }); + expect(reversedSuccessScopes.stored).toEqual([]); + }); +}); diff --git a/src/auth/__tests__/discovery.test.ts b/src/auth/__tests__/discovery.test.ts new file mode 100644 index 0000000..4fb1b23 --- /dev/null +++ b/src/auth/__tests__/discovery.test.ts @@ -0,0 +1,147 @@ +import { describe, expect, test, vi } from "vitest"; + +import { CliError } from "../../errors.js"; +import type { AuthHttpRequest, AuthHttpResponse, AuthHttpTransport } from "../../http.js"; +import { + AUTHORIZATION_SERVER_METADATA_URL, + PROTECTED_RESOURCE_METADATA_URL, + deriveAuthorizationServerMetadataUrl, + deriveProtectedResourceMetadataUrl, + discoverAuthorization, + parseResourceMetadataChallenge, +} from "../discovery.js"; + +const prm = { + authorization_servers: ["https://agentcommunity.org"], + bearer_methods_supported: ["header"], + resource: "https://agentcommunity.org/api", + resource_documentation: "https://agentcommunity.org/auth.md", + resource_name: "Agent Community agent API", + resource_policy_uri: "https://agentcommunity.org/terms", + scopes_supported: ["agent.account.read", "agent.registrations.read"], +}; + +const asMetadata = { + agent_auth: { + claim_endpoint: "https://agentcommunity.org/agent/identity/claim", + identity_endpoint: "https://agentcommunity.org/agent/identity", + identity_types_supported: ["service_auth"], + skill: "https://agentcommunity.org/auth.md", + }, + grant_types_supported: [ + "urn:ietf:params:oauth:grant-type:jwt-bearer", + "urn:workos:agent-auth:grant-type:claim", + ], + issuer: "https://agentcommunity.org", + jwks_uri: "https://agentcommunity.org/.well-known/jwks.json", + protected_resources: ["https://agentcommunity.org/api"], + revocation_endpoint: "https://agentcommunity.org/oauth2/revoke", + revocation_endpoint_auth_methods_supported: ["none"], + scopes_supported: ["agent.account.read", "agent.registrations.read"], + token_endpoint: "https://agentcommunity.org/oauth2/token", + token_endpoint_auth_methods_supported: ["none"], +}; + +function jsonResponse(status: number, body: unknown, headers: Record = {}): AuthHttpResponse { + return { + status, + headers: { "content-type": "application/json", ...headers }, + body: Buffer.from(JSON.stringify(body)), + }; +} + +function challengeResponse(value = `Bearer resource_metadata="${PROTECTED_RESOURCE_METADATA_URL}"`): AuthHttpResponse { + return { status: 401, headers: { "www-authenticate": value }, body: new Uint8Array() }; +} + +function transportWith( + protectedResource = challengeResponse(), + protectedMetadata = jsonResponse(200, prm), + authorizationMetadata = jsonResponse(200, asMetadata), +): { transport: AuthHttpTransport; requests: Array } { + const requests: Array = []; + const responses = [protectedResource, protectedMetadata, authorizationMetadata]; + return { + requests, + transport: { + requestAuth: vi.fn(async (request) => { + requests.push(request); + const response = responses.shift(); + if (response === undefined) throw new Error("unexpected request"); + return response; + }), + }, + }; +} + +describe("strict path-scoped authorization discovery", () => { + test("derives the RFC 9728 path PRM and RFC 8414 issuer metadata paths", () => { + expect(deriveProtectedResourceMetadataUrl("https://agentcommunity.org/api")).toBe(PROTECTED_RESOURCE_METADATA_URL); + expect(deriveAuthorizationServerMetadataUrl("https://agentcommunity.org")).toBe(AUTHORIZATION_SERVER_METADATA_URL); + expect(() => deriveProtectedResourceMetadataUrl("https://agentcommunity.org/api?secret=value")).toThrow(CliError); + expect(() => deriveAuthorizationServerMetadataUrl("http://agentcommunity.org")).toThrow(CliError); + }); + + test("parses only the exact quoted RFC 9728 path metadata challenge", () => { + expect(parseResourceMetadataChallenge(`Bearer realm="api", resource_metadata="${PROTECTED_RESOURCE_METADATA_URL}"`)) + .toBe(PROTECTED_RESOURCE_METADATA_URL); + expect(() => parseResourceMetadataChallenge(`Bearer resource_metadata=https://agentcommunity.org/.well-known/oauth-protected-resource/api`)) + .toThrow(CliError); + expect(() => parseResourceMetadataChallenge(`Bearer resource_metadata="https://agentcommunity.org/.well-known/oauth-protected-resource"`)) + .toThrow(CliError); + expect(() => parseResourceMetadataChallenge(`Basic realm="api"`)).toThrow(CliError); + }); + + test("discovers PRM and AS metadata from an unauthenticated resource challenge", async () => { + const { transport, requests } = transportWith(); + const discovery = await discoverAuthorization(transport, 10_000); + + expect(discovery).toMatchObject({ + issuer: "https://agentcommunity.org", + resource: "https://agentcommunity.org/api", + identityEndpoint: "https://agentcommunity.org/agent/identity", + claimEndpoint: "https://agentcommunity.org/agent/identity/claim", + tokenEndpoint: "https://agentcommunity.org/oauth2/token", + revocationEndpoint: "https://agentcommunity.org/oauth2/revoke", + scopes: ["agent.account.read", "agent.registrations.read"], + }); + expect(requests.map((request) => request.url)).toEqual([ + "https://agentcommunity.org/api", + PROTECTED_RESOURCE_METADATA_URL, + AUTHORIZATION_SERVER_METADATA_URL, + ]); + expect(requests[0]).toMatchObject({ method: "GET", headers: { Accept: "application/json" } }); + expect(requests.every((request) => Object.keys(request.headers).every((name) => name.toLowerCase() !== "authorization"))).toBe(true); + }); + + test.each([ + ["resource does not challenge", jsonResponse(200, {}), jsonResponse(200, prm), jsonResponse(200, asMetadata)], + ["challenge is missing", { status: 401, headers: {}, body: new Uint8Array() }, jsonResponse(200, prm), jsonResponse(200, asMetadata)], + ["PRM redirects", challengeResponse(), jsonResponse(302, prm), jsonResponse(200, asMetadata)], + ["PRM has wrong resource", challengeResponse(), jsonResponse(200, { ...prm, resource: "https://agentcommunity.org/" }), jsonResponse(200, asMetadata)], + ["PRM has a non-header bearer method", challengeResponse(), jsonResponse(200, { ...prm, bearer_methods_supported: ["body"] }), jsonResponse(200, asMetadata)], + ["PRM has an incomplete scope set", challengeResponse(), jsonResponse(200, { ...prm, scopes_supported: ["agent.account.read"] }), jsonResponse(200, asMetadata)], + ["PRM has a non-HTTPS issuer", challengeResponse(), jsonResponse(200, { ...prm, authorization_servers: ["http://agentcommunity.org"] }), jsonResponse(200, asMetadata)], + ["PRM has an unallowlisted issuer", challengeResponse(), jsonResponse(200, { ...prm, authorization_servers: ["https://login.example"] }), jsonResponse(200, asMetadata)], + ["AS redirects", challengeResponse(), jsonResponse(200, prm), jsonResponse(307, asMetadata)], + ["AS issuer differs", challengeResponse(), jsonResponse(200, prm), jsonResponse(200, { ...asMetadata, issuer: "https://other.example" })], + ["AS protected resource differs", challengeResponse(), jsonResponse(200, prm), jsonResponse(200, { ...asMetadata, protected_resources: ["https://agentcommunity.org/other"] })], + ["AS scopes differ", challengeResponse(), jsonResponse(200, prm), jsonResponse(200, { ...asMetadata, scopes_supported: ["agent.account.read"] })], + ["AS omits service_auth", challengeResponse(), jsonResponse(200, prm), jsonResponse(200, { ...asMetadata, agent_auth: { ...asMetadata.agent_auth, identity_types_supported: [] } })], + ["AS mixes endpoint origins", challengeResponse(), jsonResponse(200, prm), jsonResponse(200, { ...asMetadata, token_endpoint: "https://tokens.example/oauth2/token" })], + ["AS uses a non-HTTPS endpoint", challengeResponse(), jsonResponse(200, prm), jsonResponse(200, { ...asMetadata, revocation_endpoint: "http://agentcommunity.org/oauth2/revoke" })], + ["AS omits a grant", challengeResponse(), jsonResponse(200, prm), jsonResponse(200, { ...asMetadata, grant_types_supported: ["urn:workos:agent-auth:grant-type:claim"] })], + ["AS omits none endpoint auth", challengeResponse(), jsonResponse(200, prm), jsonResponse(200, { ...asMetadata, token_endpoint_auth_methods_supported: [] })], + ])("fails closed when %s", async (_label, resourceResponse, prmResponse, asResponse) => { + const { transport } = transportWith(resourceResponse as AuthHttpResponse, prmResponse, asResponse); + await expect(discoverAuthorization(transport, 10_000)).rejects.toMatchObject({ exitCode: 5 }); + }); + + test.each([ + [503, 6, "upstream_unavailable"], + [429, 7, "rate_limited"], + ])("maps protected-resource HTTP %s before parsing a challenge", async (status, exitCode, code) => { + const { transport } = transportWith(jsonResponse(status, { error: "fixture" })); + await expect(discoverAuthorization(transport, 10_000)).rejects.toMatchObject({ exitCode, code }); + }); +}); diff --git a/src/auth/credential-store.ts b/src/auth/credential-store.ts new file mode 100644 index 0000000..ef08c91 --- /dev/null +++ b/src/auth/credential-store.ts @@ -0,0 +1,430 @@ +import { randomBytes } from "node:crypto"; +import { constants } from "node:fs"; +import * as fsPromises from "node:fs/promises"; +import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; + +import { z } from "zod"; + +import { CliError } from "../errors.js"; +import { AUTH_SCOPES } from "./discovery.js"; +import { PAGE_BUNDLE_VERSION, type CredentialRecord } from "./device-flow.js"; + +const CREDENTIAL_FILE = "credentials.json"; +const STORE_DIRECTORY = "agentcommunity"; +const DIRECTORY_MODE = 0o700; +const FILE_MODE = 0o600; +const MAX_CREDENTIAL_BYTES = 32_768; +const DEFAULT_LOCK_TIMEOUT_MS = 2_000; +const DEFAULT_STALE_LOCK_MS = 5 * 60 * 1_000; +const LOCK_POLL_MS = 50; + +export type CredentialFileSystem = Pick; + +export interface CredentialPathOptions { + platform: NodeJS.Platform; + homeDirectory: string; + xdgConfigHome?: string; +} + +export interface CredentialStoreOptions extends CredentialPathOptions { + uid: number; + processId: number; + fs?: CredentialFileSystem; + randomId?: () => string; + monotonicNow?: () => number; + wallNow?: () => number; + sleep?: (milliseconds: number) => Promise; + isProcessAlive?: (pid: number) => boolean; + lockTimeoutMs?: number; + staleLockMs?: number; +} + +const canonicalScopesSchema = z.array(z.enum(AUTH_SCOPES)).min(1).max(2).superRefine((value, context) => { + const canonical = AUTH_SCOPES.filter((scope) => value.includes(scope)); + if (new Set(value).size !== value.length || canonical.join("\n") !== value.join("\n")) { + context.addIssue({ code: "custom", message: "non-canonical scopes" }); + } +}); + +export const credentialRecordSchema = z.object({ + format_version: z.literal(1), + bundle_version: z.literal(PAGE_BUNDLE_VERSION), + issuer: z.literal("https://agentcommunity.org"), + resource: z.literal("https://agentcommunity.org/api"), + scopes: canonicalScopesSchema, + access_token: z.string().min(1).max(8_192).regex(/^[\u0021-\u007e]+$/), + access_token_expires_at: z.string().datetime({ offset: true }), + identity_assertion: z.string().min(1).max(8_192), + assertion_expires_at: z.string().datetime({ offset: true }), +}).strict(); + +function storeError(code = "unsafe_credential_store", message = "The local credential store is unsafe."): CliError { + return new CliError(code, message, 4); +} + +function errorCode(error: unknown): string | undefined { + if (typeof error !== "object" || error === null || !("code" in error)) return undefined; + const code = (error as { code?: unknown }).code; + return typeof code === "string" ? code : undefined; +} + +function defaultProcessAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch (error) { + return errorCode(error) === "EPERM"; + } +} + +function requireAbsoluteRoot(value: string): string { + if (!isAbsolute(value) || value.includes("\0")) throw storeError(); + const normalized = resolve(value); + if (normalized !== value.replace(new RegExp(`${sep}+$`), "") && normalized !== value) throw storeError(); + return normalized; +} + +export function credentialPath(options: CredentialPathOptions): string { + const home = requireAbsoluteRoot(options.homeDirectory); + if (options.platform === "darwin") { + return join(home, "Library", "Application Support", STORE_DIRECTORY, CREDENTIAL_FILE); + } + if (options.platform === "linux") { + const configHome = options.xdgConfigHome === undefined || options.xdgConfigHome === "" + ? join(home, ".config") + : requireAbsoluteRoot(options.xdgConfigHome); + return join(configHome, STORE_DIRECTORY, CREDENTIAL_FILE); + } + throw storeError("unsupported_platform", "Credential storage is supported only on macOS and Linux."); +} + +interface PathPlan { + base: string; + segments: Array; +} + +function pathPlan(options: CredentialPathOptions): PathPlan { + const home = requireAbsoluteRoot(options.homeDirectory); + if (options.platform === "darwin") return { base: home, segments: ["Library", "Application Support", STORE_DIRECTORY] }; + if (options.platform !== "linux") throw storeError("unsupported_platform", "Credential storage is supported only on macOS and Linux."); + if (options.xdgConfigHome === undefined || options.xdgConfigHome === "") return { base: home, segments: [".config", STORE_DIRECTORY] }; + const xdg = requireAbsoluteRoot(options.xdgConfigHome); + const fromHome = relative(home, xdg); + if (fromHome !== "" && !fromHome.startsWith(`..${sep}`) && fromHome !== ".." && !isAbsolute(fromHome)) { + return { base: home, segments: [...fromHome.split(sep), STORE_DIRECTORY] }; + } + return { base: xdg, segments: [STORE_DIRECTORY] }; +} + +export class PosixCredentialStore { + readonly path: string; + private readonly options: Required> & Pick; + private readonly fs: CredentialFileSystem; + + constructor(options: CredentialStoreOptions) { + this.path = credentialPath(options); + this.fs = options.fs ?? fsPromises; + this.options = { + platform: options.platform, + homeDirectory: options.homeDirectory, + ...(options.xdgConfigHome === undefined ? {} : { xdgConfigHome: options.xdgConfigHome }), + uid: options.uid, + processId: options.processId, + randomId: options.randomId ?? (() => randomBytes(16).toString("hex")), + monotonicNow: options.monotonicNow ?? (() => performance.now()), + wallNow: options.wallNow ?? Date.now, + sleep: options.sleep ?? ((milliseconds) => new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds))), + isProcessAlive: options.isProcessAlive ?? defaultProcessAlive, + lockTimeoutMs: options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS, + staleLockMs: options.staleLockMs ?? DEFAULT_STALE_LOCK_MS, + }; + } + + async read(): Promise { + const directory = await this.ensureDirectory(false); + if (directory === null) return null; + return this.readCredentialFile(); + } + + async write(value: CredentialRecord): Promise { + const parsed = credentialRecordSchema.safeParse(value); + if (!parsed.success) throw storeError("invalid_credential_record", "The credential record is invalid."); + const directory = await this.ensureDirectory(true); + if (directory === null) throw storeError(); + await this.withLock(directory, async () => { + await this.validateCredentialTarget(); + await this.atomicWrite(directory, parsed.data); + }); + } + + async replace(expected: CredentialRecord, value: CredentialRecord): Promise { + const expectedResult = credentialRecordSchema.safeParse(expected); + const valueResult = credentialRecordSchema.safeParse(value); + if (!expectedResult.success || !valueResult.success) throw storeError("invalid_credential_record", "The credential record is invalid."); + const directory = await this.ensureDirectory(false); + if (directory === null) return false; + return this.withLock(directory, async () => { + const current = await this.readCredentialFile(); + if (current === null || JSON.stringify(current) !== JSON.stringify(expectedResult.data)) return false; + await this.atomicWrite(directory, valueResult.data); + return true; + }); + } + + async remove(expected?: CredentialRecord): Promise { + const directory = await this.ensureDirectory(false); + if (directory === null) return false; + const before = await this.safeLstat(this.path); + if (before === null) return false; + this.requireSafeFile(before); + return this.withLock(directory, async () => { + const current = await this.safeLstat(this.path); + if (current === null) return false; + this.requireSafeFile(current); + if (current.dev !== before.dev || current.ino !== before.ino) return false; + if (expected !== undefined) { + const record = await this.readCredentialFile(); + if (record === null || JSON.stringify(record) !== JSON.stringify(expected)) return false; + } + await this.fs.unlink(this.path); + await this.syncDirectory(directory); + return true; + }); + } + + private async safeLstat(path: string): Promise> | null> { + try { + return await this.fs.lstat(path); + } catch (error) { + if (errorCode(error) === "ENOENT") return null; + throw storeError(); + } + } + + private requireSafeDirectory(stat: Awaited>, exactMode: boolean): void { + const mode = Number(stat.mode) & 0o777; + if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== this.options.uid || (exactMode ? mode !== DIRECTORY_MODE : (mode & 0o022) !== 0)) { + throw storeError(); + } + } + + private requireSafeFile(stat: Awaited>): void { + if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== this.options.uid || stat.nlink !== 1 || (Number(stat.mode) & 0o777) !== FILE_MODE) { + throw storeError(); + } + } + + private async ensureDirectory(create: boolean): Promise { + const plan = pathPlan(this.options); + let current = plan.base; + let baseStat = await this.safeLstat(current); + if (baseStat === null) { + if (!create || current !== requireAbsoluteRoot(this.options.xdgConfigHome ?? this.options.homeDirectory)) return null; + const parent = dirname(current); + const parentStat = await this.safeLstat(parent); + if (parentStat === null) throw storeError(); + if (!parentStat.isDirectory() || parentStat.isSymbolicLink() || (Number(parentStat.mode) & 0o022) !== 0) throw storeError(); + try { + await this.fs.mkdir(current, { mode: DIRECTORY_MODE }); + } catch (error) { + if (errorCode(error) !== "EEXIST") throw storeError(); + } + baseStat = await this.safeLstat(current); + } + if (baseStat === null) return null; + this.requireSafeDirectory(baseStat, false); + for (const [index, segment] of plan.segments.entries()) { + current = join(current, segment); + let stat = await this.safeLstat(current); + if (stat === null) { + if (!create) return null; + try { + await this.fs.mkdir(current, { mode: DIRECTORY_MODE }); + } catch (error) { + if (errorCode(error) !== "EEXIST") throw storeError(); + } + stat = await this.safeLstat(current); + } + if (stat === null) throw storeError(); + this.requireSafeDirectory(stat, index === plan.segments.length - 1); + } + if (current !== dirname(this.path)) throw storeError(); + return current; + } + + private async validateCredentialTarget(): Promise { + const stat = await this.safeLstat(this.path); + if (stat !== null) this.requireSafeFile(stat); + } + + private async readCredentialFile(): Promise { + const before = await this.safeLstat(this.path); + if (before === null) return null; + this.requireSafeFile(before); + if (before.size > MAX_CREDENTIAL_BYTES) throw storeError(); + let handle: Awaited>; + try { + handle = await this.fs.open(this.path, constants.O_RDONLY | constants.O_NOFOLLOW); + } catch { + throw storeError(); + } + try { + const after = await handle.stat(); + this.requireSafeFile(after); + if (after.dev !== before.dev || after.ino !== before.ino || after.size > MAX_CREDENTIAL_BYTES) throw storeError(); + const bytes = await handle.readFile(); + let value: unknown; + try { + value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)); + } catch { + throw storeError("corrupt_credential_store", "The local credential record is corrupt."); + } + const parsed = credentialRecordSchema.safeParse(value); + if (!parsed.success) throw storeError("corrupt_credential_store", "The local credential record is corrupt."); + return parsed.data; + } finally { + await handle.close(); + } + } + + private async withLock(directory: string, operation: () => Promise): Promise { + const lockPath = `${this.path}.lock`; + const started = this.options.monotonicNow(); + let lockHandle: Awaited> | undefined; + let lockIdentity: { dev: bigint | number; ino: bigint | number } | undefined; + while (lockHandle === undefined) { + let candidate: Awaited> | undefined; + let candidateIdentity: { dev: bigint | number; ino: bigint | number } | undefined; + try { + candidate = await this.fs.open( + lockPath, + constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, + FILE_MODE, + ); + const stat = await candidate.stat(); + candidateIdentity = { dev: stat.dev, ino: stat.ino }; + this.requireSafeFile(stat); + await candidate.writeFile(`${JSON.stringify({ pid: this.options.processId, created_at_ms: this.options.wallNow() })}\n`, "utf8"); + await candidate.sync(); + lockHandle = candidate; + lockIdentity = candidateIdentity; + candidate = undefined; + } catch (error) { + if (candidate !== undefined) { + await candidate.close().catch(() => undefined); + if (candidateIdentity !== undefined) { + const current = await this.safeLstat(lockPath).catch(() => null); + if (current !== null && current.dev === candidateIdentity.dev && current.ino === candidateIdentity.ino) { + await this.fs.unlink(lockPath).catch(() => undefined); + await this.syncDirectory(directory).catch(() => undefined); + } + } + } + if (errorCode(error) !== "EEXIST") throw storeError("credential_lock_failed", "The credential store lock could not be acquired."); + await this.considerStaleLock(lockPath); + if (this.options.monotonicNow() - started >= this.options.lockTimeoutMs) { + throw storeError("credential_store_locked", "The credential store is locked by another process."); + } + await this.options.sleep(Math.min(LOCK_POLL_MS, this.options.lockTimeoutMs)); + } + } + try { + return await operation(); + } finally { + await lockHandle.close().catch(() => undefined); + if (lockIdentity !== undefined) { + const current = await this.safeLstat(lockPath).catch(() => null); + if (current !== null && current.dev === lockIdentity.dev && current.ino === lockIdentity.ino) { + await this.fs.unlink(lockPath).catch(() => undefined); + await this.syncDirectory(directory).catch(() => undefined); + } + } + } + } + + private async considerStaleLock(lockPath: string): Promise { + const before = await this.safeLstat(lockPath); + if (before === null) return; + this.requireSafeFile(before); + if (before.size > 1_024) return; + let value: unknown; + let handle: Awaited> | undefined; + try { + handle = await this.fs.open(lockPath, constants.O_RDONLY | constants.O_NOFOLLOW); + const opened = await handle.stat(); + this.requireSafeFile(opened); + if (opened.dev !== before.dev || opened.ino !== before.ino || opened.size > 1_024) return; + value = JSON.parse(await handle.readFile("utf8")); + } catch { + return; + } finally { + if (handle !== undefined) await handle.close().catch(() => undefined); + } + const parsed = z.object({ pid: z.number().int().positive(), created_at_ms: z.number().finite().nonnegative() }).strict().safeParse(value); + if (!parsed.success) return; + if (this.options.wallNow() - parsed.data.created_at_ms < this.options.staleLockMs || this.options.isProcessAlive(parsed.data.pid)) return; + const current = await this.safeLstat(lockPath); + if (current === null || current.dev !== before.dev || current.ino !== before.ino) return; + this.requireSafeFile(current); + await this.fs.unlink(lockPath); + } + + private async atomicWrite(directory: string, value: CredentialRecord): Promise { + const directoryBefore = await this.safeLstat(directory); + if (directoryBefore === null) throw storeError(); + this.requireSafeDirectory(directoryBefore, true); + const randomId = this.options.randomId(); + if (!/^[A-Za-z0-9_-]{1,64}$/.test(randomId)) throw storeError("credential_write_failed", "The credential record could not be stored safely."); + const tempPath = join(directory, `.${CREDENTIAL_FILE}.tmp-${this.options.processId}-${randomId}`); + let handle: Awaited> | undefined; + let renamed = false; + try { + handle = await this.fs.open( + tempPath, + constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, + FILE_MODE, + ); + const opened = await handle.stat(); + this.requireSafeFile(opened); + await handle.writeFile(`${JSON.stringify(value)}\n`, "utf8"); + await handle.sync(); + const complete = await handle.stat(); + this.requireSafeFile(complete); + await handle.close(); + handle = undefined; + await this.fs.rename(tempPath, this.path); + renamed = true; + const directoryAfter = await this.safeLstat(directory); + if (directoryAfter === null || directoryAfter.dev !== directoryBefore.dev || directoryAfter.ino !== directoryBefore.ino) throw storeError(); + this.requireSafeDirectory(directoryAfter, true); + const target = await this.safeLstat(this.path); + if (target === null || target.dev !== complete.dev || target.ino !== complete.ino) throw storeError(); + this.requireSafeFile(target); + await this.syncDirectory(directory); + } catch (error) { + if (error instanceof CliError) throw error; + throw storeError("credential_write_failed", "The credential record could not be stored safely."); + } finally { + if (handle !== undefined) await handle.close().catch(() => undefined); + if (!renamed) await this.fs.unlink(tempPath).catch(() => undefined); + } + } + + private async syncDirectory(directory: string): Promise { + let handle: Awaited>; + try { + handle = await this.fs.open(directory, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW); + } catch { + throw storeError(); + } + try { + const stat = await handle.stat(); + this.requireSafeDirectory(stat, true); + await handle.sync(); + } finally { + await handle.close(); + } + } +} diff --git a/src/auth/device-flow.ts b/src/auth/device-flow.ts new file mode 100644 index 0000000..cfc36dc --- /dev/null +++ b/src/auth/device-flow.ts @@ -0,0 +1,272 @@ +import { z } from "zod"; + +import { usageError, CliError } from "../errors.js"; +import type { AuthHttpResponse, AuthHttpTransport } from "../http.js"; +import { + AUTH_SCOPES, + CLAIM_GRANT, + type AuthorizationDiscovery, +} from "./discovery.js"; + +export const DEFAULT_AUTH_DEADLINE_MS = 15 * 60 * 1_000; +export const MAX_AUTH_DEADLINE_MS = 30 * 60 * 1_000; +export const PAGE_BUNDLE_VERSION = "1.0.0"; + +type AuthScope = (typeof AUTH_SCOPES)[number]; + +export interface CredentialRecord { + format_version: 1; + bundle_version: typeof PAGE_BUNDLE_VERSION; + issuer: "https://agentcommunity.org"; + resource: "https://agentcommunity.org/api"; + scopes: Array; + access_token: string; + access_token_expires_at: string; + identity_assertion: string; + assertion_expires_at: string; +} + +export interface VerificationDetails { + verificationUri: string; + userCode: string; +} + +export interface ServiceAuthLoginOptions { + http: AuthHttpTransport; + discovery: AuthorizationDiscovery; + loginHint: string; + requestedScopes: Array; + timeoutMs: number; + deadlineMs?: number; + monotonicNow(): number; + wallNow(): number; + sleep(milliseconds: number): Promise; + presentVerification(value: VerificationDetails): void; + store(value: CredentialRecord): Promise; +} + +const emailSchema = z.string().trim().min(3).max(320).email(); +const opaqueSchema = z.string().min(1).max(8_192).regex(/^[\u0021-\u007e]+$/); +const isoDateSchema = z.string().datetime({ offset: true }); +const uuidSchema = z.string().uuid(); +const authErrorSchema = z.object({ error: z.enum([ + "invalid_request", + "unsupported_grant_type", + "invalid_target", + "invalid_grant", + "authorization_pending", + "slow_down", + "access_denied", + "expired_token", + "invalid_claim_token", + "claim_expired", + "claimed_or_in_flight", + "rate_limited", + "temporarily_unavailable", +]), error_description: z.string() }).strict(); + +const errorDescriptions: Record["error"], string> = { + invalid_request: "The request is malformed", + unsupported_grant_type: "The grant type is not supported", + invalid_target: "The requested resource is invalid", + invalid_grant: "The authorization grant is invalid", + authorization_pending: "Authorization is still pending", + slow_down: "Polling too quickly; increase the interval by 5 seconds", + access_denied: "Authorization was denied", + expired_token: "The claim token or current claim attempt has expired", + invalid_claim_token: "The claim token is invalid", + claim_expired: "The registration claim has expired", + claimed_or_in_flight: "The registration is already claimed or has an active attempt", + rate_limited: "Too many requests", + temporarily_unavailable: "The authorization service is temporarily unavailable", +}; + +function authProtocolError(): CliError { + return new CliError("auth_response_mismatch", "The authorization response did not match the pinned PAGE contract.", 5); +} + +function parseJson(response: AuthHttpResponse): unknown { + const contentType = response.headers["content-type"]; + if (contentType === undefined || !/^application\/json(?:\s*;\s*charset=utf-8)?$/i.test(contentType.trim())) throw authProtocolError(); + try { + return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(response.body)); + } catch { + throw authProtocolError(); + } +} + +function canonicalScopes(value: Array): Array { + if (value.length < 1 || value.length > AUTH_SCOPES.length || new Set(value).size !== value.length) { + throw usageError("invalid_auth_scope", "Auth scopes must be unique supported PAGE scopes."); + } + const canonical = AUTH_SCOPES.filter((scope) => value.includes(scope)); + if (canonical.length !== value.length) throw usageError("invalid_auth_scope", "Auth scopes must be unique supported PAGE scopes."); + return canonical; +} + +function requireResponseScopes(scope: string, expected: Array): Array { + const parts = scope.split(" "); + if (parts.some((part) => part.length === 0)) throw authProtocolError(); + const parsed = z.array(z.enum(AUTH_SCOPES)).safeParse(parts); + if (!parsed.success) throw authProtocolError(); + const canonical = AUTH_SCOPES.filter((value) => parsed.data.includes(value)); + if ( + new Set(parsed.data).size !== parsed.data.length + || canonical.join(" ") !== expected.join(" ") + || parsed.data.join(" ") !== expected.join(" ") + ) throw authProtocolError(); + return canonical; +} + +function requireVerificationUri(value: string): string { + let parsed: URL; + try { + parsed = new URL(value); + } catch { + throw authProtocolError(); + } + if ( + parsed.origin !== "https://agentcommunity.org" + || parsed.pathname !== "/agent/authorize" + || parsed.hash !== "" + || parsed.searchParams.size !== 1 + || parsed.searchParams.getAll("claim_attempt_token").length !== 1 + || !opaqueSchema.safeParse(parsed.searchParams.get("claim_attempt_token")).success + ) throw authProtocolError(); + return value; +} + +export function normalizeLoginHint(value: string): string { + const parsed = emailSchema.safeParse(value); + if (!parsed.success) throw usageError("invalid_login_hint", "--login-hint must be a valid email address."); + return parsed.data; +} + +export function normalizeRequestedScopes(value: Array): Array { + const parsed = z.array(z.enum(AUTH_SCOPES)).safeParse(value); + if (!parsed.success) throw usageError("invalid_auth_scope", "Auth scopes must be unique supported PAGE scopes."); + return canonicalScopes(parsed.data); +} + +export async function runServiceAuthLogin(options: ServiceAuthLoginOptions): Promise { + const deadlineMs = options.deadlineMs ?? DEFAULT_AUTH_DEADLINE_MS; + if (!Number.isSafeInteger(deadlineMs) || deadlineMs <= 0 || deadlineMs > MAX_AUTH_DEADLINE_MS) { + throw usageError("invalid_auth_deadline", "The local authorization deadline must be at most 30 minutes."); + } + const loginHint = normalizeLoginHint(options.loginHint); + const requestedScopes = canonicalScopes(options.requestedScopes); + const deadlineAt = options.monotonicNow() + deadlineMs; + + const startResponse = await options.http.requestAuth({ + method: "POST", + url: options.discovery.identityEndpoint, + timeoutMs: options.timeoutMs, + maxBytes: 16_384, + headers: { Accept: "application/json", "Content-Type": "application/json" }, + body: JSON.stringify({ type: "service_auth", login_hint: loginHint, scopes: requestedScopes, client_name: "@agentcommunity/cli" }), + }); + if (startResponse.status === 429) throw new CliError("rate_limited", "The authorization service rate limit was reached.", 7); + if (startResponse.status >= 500) throw new CliError("upstream_unavailable", "The Agent Community authorization service is temporarily unavailable.", 6); + if (startResponse.status !== 200) throw authProtocolError(); + const startSchema = z.object({ + registration_id: uuidSchema, + registration_type: z.literal("service_auth"), + claim_url: z.literal(options.discovery.claimEndpoint), + claim_token: opaqueSchema, + claim_token_expires: isoDateSchema, + post_claim_scopes: z.array(z.enum(AUTH_SCOPES)).min(1).max(2), + claim: z.object({ + user_code: z.string().regex(/^\d{6}$/), + expires_in: z.literal(600), + verification_uri: z.string(), + interval: z.literal(5), + }).strict(), + }).strict(); + const start = startSchema.safeParse(parseJson(startResponse)); + if (!start.success) throw authProtocolError(); + const responseScopes = AUTH_SCOPES.filter((scope) => start.data.post_claim_scopes.includes(scope)); + if ( + new Set(start.data.post_claim_scopes).size !== start.data.post_claim_scopes.length + || responseScopes.join(" ") !== requestedScopes.join(" ") + || start.data.post_claim_scopes.join(" ") !== requestedScopes.join(" ") + ) throw authProtocolError(); + const verificationUri = requireVerificationUri(start.data.claim.verification_uri); + if (Date.parse(start.data.claim_token_expires) <= options.wallNow()) throw authProtocolError(); + + options.presentVerification({ verificationUri, userCode: start.data.claim.user_code }); + let intervalMs = start.data.claim.interval * 1_000; + let lastPollWasUnavailable = false; + function deadlineError(): CliError { + return lastPollWasUnavailable + ? new CliError("authorization_unavailable", "The authorization service remained unavailable until the local deadline.", 6) + : new CliError("authorization_timeout", "The local authorization deadline expired.", 4); + } + while (true) { + if (options.monotonicNow() + intervalMs > deadlineAt) throw deadlineError(); + await options.sleep(intervalMs); + if (options.monotonicNow() >= deadlineAt) throw deadlineError(); + let pollResponse: AuthHttpResponse; + try { + pollResponse = await options.http.requestAuth({ + method: "POST", + url: options.discovery.tokenEndpoint, + timeoutMs: options.timeoutMs, + maxBytes: 16_384, + headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ grant_type: CLAIM_GRANT, claim_token: start.data.claim_token }).toString(), + }); + } catch (error) { + if (error instanceof CliError && error.exitCode !== 6) throw error; + lastPollWasUnavailable = true; + continue; + } + if (pollResponse.status >= 500) { + lastPollWasUnavailable = true; + continue; + } + lastPollWasUnavailable = false; + const value = parseJson(pollResponse); + if (pollResponse.status === 400 || pollResponse.status === 401 || pollResponse.status === 409 || pollResponse.status === 410 || pollResponse.status === 429) { + const parsedError = authErrorSchema.safeParse(value); + if (!parsedError.success || parsedError.data.error_description !== errorDescriptions[parsedError.data.error]) throw authProtocolError(); + switch (parsedError.data.error) { + case "authorization_pending": break; + case "slow_down": intervalMs += 5_000; break; + case "access_denied": throw new CliError("authorization_denied", "Authorization was denied.", 4); + case "expired_token": + case "claim_expired": + case "invalid_claim_token": throw new CliError("authorization_expired", "The authorization claim expired.", 4); + case "rate_limited": throw new CliError("rate_limited", "The authorization service rate limit was reached.", 7); + case "temporarily_unavailable": lastPollWasUnavailable = true; continue; + default: throw authProtocolError(); + } + continue; + } + if (pollResponse.status !== 200) throw authProtocolError(); + const successSchema = z.object({ + access_token: opaqueSchema, + token_type: z.literal("Bearer"), + expires_in: z.number().int().positive().max(3_600), + scope: z.string().min(1), + identity_assertion: z.string().min(1).max(8_192), + assertion_expires: isoDateSchema, + }).strict(); + const success = successSchema.safeParse(value); + if (!success.success) throw authProtocolError(); + const scopes = requireResponseScopes(success.data.scope, requestedScopes); + if (Date.parse(success.data.assertion_expires) <= options.wallNow()) throw authProtocolError(); + const credential: CredentialRecord = { + format_version: 1, + bundle_version: PAGE_BUNDLE_VERSION, + issuer: options.discovery.issuer, + resource: options.discovery.resource, + scopes, + access_token: success.data.access_token, + access_token_expires_at: new Date(options.wallNow() + success.data.expires_in * 1_000).toISOString(), + identity_assertion: success.data.identity_assertion, + assertion_expires_at: success.data.assertion_expires, + }; + await options.store(credential); + return credential; + } +} diff --git a/src/auth/discovery.ts b/src/auth/discovery.ts new file mode 100644 index 0000000..e1ce32a --- /dev/null +++ b/src/auth/discovery.ts @@ -0,0 +1,171 @@ +import { z } from "zod"; + +import { AGENT_COMMUNITY_ORIGIN, AGENT_COMMUNITY_RESOURCE } from "../config.js"; +import { CliError } from "../errors.js"; +import type { AuthHttpResponse, AuthHttpTransport } from "../http.js"; + +export const PROTECTED_RESOURCE_METADATA_URL = "https://agentcommunity.org/.well-known/oauth-protected-resource/api"; +export const AUTHORIZATION_SERVER_METADATA_URL = "https://agentcommunity.org/.well-known/oauth-authorization-server"; +export const AUTH_SCOPES = ["agent.account.read", "agent.registrations.read"] as const; +export const CLAIM_GRANT = "urn:workos:agent-auth:grant-type:claim"; +export const JWT_BEARER_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer"; + +const exactScopesSchema = z.array(z.enum(AUTH_SCOPES)).length(2).superRefine((value, context) => { + if (new Set(value).size !== 2 || !AUTH_SCOPES.every((scope) => value.includes(scope))) { + context.addIssue({ code: "custom", message: "scope set mismatch" }); + } +}); + +const protectedResourceMetadataSchema = z.object({ + authorization_servers: z.tuple([z.literal(AGENT_COMMUNITY_ORIGIN)]), + bearer_methods_supported: z.tuple([z.literal("header")]), + resource: z.literal(AGENT_COMMUNITY_RESOURCE), + resource_documentation: z.literal(`${AGENT_COMMUNITY_ORIGIN}/auth.md`), + resource_name: z.literal("Agent Community agent API"), + resource_policy_uri: z.literal(`${AGENT_COMMUNITY_ORIGIN}/terms`), + scopes_supported: exactScopesSchema, +}).strict(); + +const authorizationServerMetadataSchema = z.object({ + agent_auth: z.object({ + claim_endpoint: z.literal(`${AGENT_COMMUNITY_ORIGIN}/agent/identity/claim`), + identity_endpoint: z.literal(`${AGENT_COMMUNITY_ORIGIN}/agent/identity`), + identity_types_supported: z.tuple([z.literal("service_auth")]), + skill: z.literal(`${AGENT_COMMUNITY_ORIGIN}/auth.md`), + }).strict(), + grant_types_supported: z.array(z.enum([JWT_BEARER_GRANT, CLAIM_GRANT])).length(2).superRefine((value, context) => { + if (new Set(value).size !== 2 || !value.includes(JWT_BEARER_GRANT) || !value.includes(CLAIM_GRANT)) { + context.addIssue({ code: "custom", message: "grant set mismatch" }); + } + }), + issuer: z.literal(AGENT_COMMUNITY_ORIGIN), + jwks_uri: z.literal(`${AGENT_COMMUNITY_ORIGIN}/.well-known/jwks.json`), + protected_resources: z.tuple([z.literal(AGENT_COMMUNITY_RESOURCE)]), + revocation_endpoint: z.literal(`${AGENT_COMMUNITY_ORIGIN}/oauth2/revoke`), + revocation_endpoint_auth_methods_supported: z.tuple([z.literal("none")]), + scopes_supported: exactScopesSchema, + token_endpoint: z.literal(`${AGENT_COMMUNITY_ORIGIN}/oauth2/token`), + token_endpoint_auth_methods_supported: z.tuple([z.literal("none")]), +}).strict(); + +export interface AuthorizationDiscovery { + issuer: typeof AGENT_COMMUNITY_ORIGIN; + resource: typeof AGENT_COMMUNITY_RESOURCE; + scopes: Array<(typeof AUTH_SCOPES)[number]>; + identityEndpoint: string; + claimEndpoint: string; + tokenEndpoint: string; + revocationEndpoint: string; +} + +function protocolError(): CliError { + return new CliError("auth_discovery_mismatch", "Authorization discovery did not match the pinned PAGE contract.", 5); +} + +function metadataSourceUrl(value: string): URL { + let parsed: URL; + try { + parsed = new URL(value); + } catch { + throw protocolError(); + } + if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "" || parsed.search !== "" || parsed.hash !== "") throw protocolError(); + return parsed; +} + +export function deriveProtectedResourceMetadataUrl(resource: string): string { + const parsed = metadataSourceUrl(resource); + const suffix = parsed.pathname === "/" ? "" : parsed.pathname; + return `${parsed.origin}/.well-known/oauth-protected-resource${suffix}`; +} + +export function deriveAuthorizationServerMetadataUrl(issuer: string): string { + const parsed = metadataSourceUrl(issuer); + const suffix = parsed.pathname === "/" ? "" : parsed.pathname; + return `${parsed.origin}/.well-known/oauth-authorization-server${suffix}`; +} + +function parseJsonResponse(response: AuthHttpResponse): unknown { + if (response.status >= 300 && response.status < 400) throw protocolError(); + if (response.status === 429) throw new CliError("rate_limited", "The service rate limit was reached.", 7); + if (response.status >= 500) throw new CliError("upstream_unavailable", "The Agent Community authorization service is temporarily unavailable.", 6); + if (response.status !== 200) throw protocolError(); + const contentType = response.headers["content-type"]; + if (contentType === undefined || !/^application\/json(?:\s*;\s*charset=utf-8)?$/i.test(contentType.trim())) throw protocolError(); + try { + return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(response.body)); + } catch { + throw protocolError(); + } +} + +export function parseResourceMetadataChallenge(value: string): string { + if (!/^Bearer(?:\s|$)/i.test(value)) throw protocolError(); + const parameters = value.replace(/^Bearer\s*/i, ""); + const matches = [...parameters.matchAll(/(?:^|,)\s*resource_metadata\s*=\s*("(?:[^"\\]|\\.)*"|[^,\s]+)/gi)]; + if (matches.length !== 1) throw protocolError(); + const encoded = matches[0]?.[1]; + if (encoded === undefined || !encoded.startsWith('"') || !encoded.endsWith('"') || encoded.includes("\\")) throw protocolError(); + const url = encoded.slice(1, -1); + if (url !== PROTECTED_RESOURCE_METADATA_URL) throw protocolError(); + return url; +} + +export async function discoverAuthorization(http: AuthHttpTransport, timeoutMs: number): Promise { + if (deriveProtectedResourceMetadataUrl(AGENT_COMMUNITY_RESOURCE) !== PROTECTED_RESOURCE_METADATA_URL) throw protocolError(); + const resourceResponse = await http.requestAuth({ + method: "GET", + url: AGENT_COMMUNITY_RESOURCE, + timeoutMs, + maxBytes: 16_384, + headers: { Accept: "application/json" }, + }); + if (resourceResponse.status === 429) throw new CliError("rate_limited", "The service rate limit was reached.", 7); + if (resourceResponse.status >= 500) throw new CliError("upstream_unavailable", "The Agent Community authorization service is temporarily unavailable.", 6); + if (resourceResponse.status !== 401) throw protocolError(); + const challenge = resourceResponse.headers["www-authenticate"]; + if (challenge === undefined) throw protocolError(); + const metadataUrl = parseResourceMetadataChallenge(challenge); + + const prmResponse = await http.requestAuth({ + method: "GET", + url: metadataUrl, + timeoutMs, + maxBytes: 16_384, + headers: { Accept: "application/json" }, + }); + const prmResult = protectedResourceMetadataSchema.safeParse(parseJsonResponse(prmResponse)); + if (!prmResult.success) throw protocolError(); + const issuer = prmResult.data.authorization_servers[0]; + if (new URL(issuer).protocol !== "https:" || new URL(issuer).origin !== AGENT_COMMUNITY_ORIGIN) throw protocolError(); + const authorizationMetadataUrl = deriveAuthorizationServerMetadataUrl(issuer); + if (authorizationMetadataUrl !== AUTHORIZATION_SERVER_METADATA_URL) throw protocolError(); + + const asResponse = await http.requestAuth({ + method: "GET", + url: authorizationMetadataUrl, + timeoutMs, + maxBytes: 32_768, + headers: { Accept: "application/json" }, + }); + const asResult = authorizationServerMetadataSchema.safeParse(parseJsonResponse(asResponse)); + if (!asResult.success || asResult.data.issuer !== issuer) throw protocolError(); + for (const endpoint of [ + asResult.data.agent_auth.identity_endpoint, + asResult.data.agent_auth.claim_endpoint, + asResult.data.token_endpoint, + asResult.data.revocation_endpoint, + ]) { + const parsed = new URL(endpoint); + if (parsed.protocol !== "https:" || parsed.origin !== issuer || parsed.username !== "" || parsed.password !== "") throw protocolError(); + } + return { + issuer, + resource: AGENT_COMMUNITY_RESOURCE, + scopes: [...AUTH_SCOPES], + identityEndpoint: asResult.data.agent_auth.identity_endpoint, + claimEndpoint: asResult.data.agent_auth.claim_endpoint, + tokenEndpoint: asResult.data.token_endpoint, + revocationEndpoint: asResult.data.revocation_endpoint, + }; +} diff --git a/src/cli.ts b/src/cli.ts index 04c007e..b247b08 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,7 +1,12 @@ import { randomUUID } from "node:crypto"; import { open, realpath } from "node:fs/promises"; +import { homedir } from "node:os"; import { pathToFileURL } from "node:url"; +import { PosixCredentialStore } from "./auth/credential-store.js"; +import { AUTH_SCOPES } from "./auth/discovery.js"; +import { normalizeLoginHint, normalizeRequestedScopes } from "./auth/device-flow.js"; +import { runAuthLogin, runAuthLogout, runAuthRevoke, runAuthStatus, type CredentialStore } from "./commands/auth.js"; import { runBatch } from "./commands/batch.js"; import { runContent, type ContentOptions } from "./commands/content.js"; import { runDocsAsk } from "./commands/docs.js"; @@ -10,14 +15,19 @@ import { runStats } from "./commands/stats.js"; import { runVerify } from "./commands/verify.js"; import { BATCH_INPUT_MAX_BYTES, parseTimeout } from "./config.js"; import { CliError, type ExitCode, usageError } from "./errors.js"; -import { HttpClient, type HttpTransport } from "./http.js"; +import { HttpClient, type AuthHttpTransport, type HttpTransport } from "./http.js"; import { McpClient, type McpTransport } from "./mcp.js"; export const CLI_VERSION = "0.1.0"; export interface CliDependencies { http: HttpTransport; + authHttp: AuthHttpTransport; mcp: McpTransport; + credentials: CredentialStore; + monotonicNow(): number; + wallNow(): number; + sleep(milliseconds: number): Promise; readFile(path: string, maxBytes?: number): Promise; readStdin(maxBytes?: number): Promise; stdout(value: string): void; @@ -27,6 +37,7 @@ export interface CliDependencies { interface GlobalOptions { json: boolean; timeoutMs: number; + timeoutSpecified: boolean; args: Array; } @@ -36,7 +47,7 @@ interface CommandResult { exitCode: ExitCode; } -const HELP = `Agent Community read-only CLI +const HELP = `Agent Community CLI Usage: agentcommunity stats [--json] [--timeout ] @@ -46,12 +57,26 @@ Usage: agentcommunity content search [--type docs|blog|page] [--limit 1..50] [--cursor opaque] [--json] [--timeout ] agentcommunity docs ask [--top-k 1..10] [--json] [--timeout ] agentcommunity batch [--json] [--timeout ] + agentcommunity auth [options] Exit codes: 0 success, 2 usage/input, 3 not found/ambiguous/not issued, -4 reserved for auth, 5 protocol/contract, 6 timeout/unavailable, +4 auth/credential safety, 5 protocol/contract, 6 timeout/unavailable, 7 rate limited, 8 mixed batch result. `; +const AUTH_HELP = `Agent Community user-claimed authorization + +Usage: + agentcommunity auth login --login-hint [--scope ...] [--json] [--timeout ] + agentcommunity auth status [--json] [--timeout ] + agentcommunity auth logout [--json] + agentcommunity auth revoke [--json] [--timeout ] + +Supported scopes: agent.account.read, agent.registrations.read. +auth logout removes only the local credential. auth revoke asks the server to +process revocation of the current access token, then removes matching local state. +`; + function parseGlobals(argv: Array): GlobalOptions { let json = false; let timeoutValue: string | undefined; @@ -70,7 +95,7 @@ function parseGlobals(argv: Array): GlobalOptions { args.push(argument); } } - return { json, timeoutMs: parseTimeout(timeoutValue), args }; + return { json, timeoutMs: parseTimeout(timeoutValue), timeoutSpecified: timeoutValue !== undefined, args }; } function parseNamedOptions(args: Array, allowed: ReadonlySet): { positional: Array; options: Record } { @@ -96,6 +121,31 @@ function exactly(args: Array, count: number, usage: string): void { if (args.length !== count) throw usageError("invalid_usage", usage); } +function parseAuthLoginOptions(args: Array): { loginHint: string; scopes: Array<(typeof AUTH_SCOPES)[number]> } { + let loginHint: string | undefined; + const scopes: Array = []; + for (let index = 0; index < args.length; index += 1) { + const argument = args[index]; + if (argument !== "--login-hint" && argument !== "--scope") { + throw usageError("unknown_option", `Unknown auth login option: ${argument ?? ""}`); + } + const value = args[index + 1]; + if (value === undefined || value.startsWith("--")) throw usageError("missing_option_value", `${argument} requires a value.`); + if (argument === "--login-hint") { + if (loginHint !== undefined) throw usageError("duplicate_option", "--login-hint may be specified only once."); + loginHint = value; + } else { + scopes.push(value); + } + index += 1; + } + if (loginHint === undefined) throw usageError("missing_login_hint", "auth login requires --login-hint ."); + return { + loginHint: normalizeLoginHint(loginHint), + scopes: normalizeRequestedScopes(scopes.length === 0 ? [...AUTH_SCOPES] : scopes), + }; +} + async function dispatch(options: GlobalOptions, dependencies: CliDependencies): Promise { const [command, ...rest] = options.args; if (command === undefined || command === "--help" || command === "-h" || command === "help") { @@ -150,14 +200,51 @@ async function dispatch(options: GlobalOptions, dependencies: CliDependencies): } return runBatch(dependencies.http, bytes, options.timeoutMs); } + if (command === "auth") { + const [subcommand, ...authArgs] = rest; + if (subcommand === undefined || subcommand === "--help" || subcommand === "-h" || subcommand === "help") { + dependencies.stdout(AUTH_HELP); + return null; + } + if (subcommand === "login") { + const parsed = parseAuthLoginOptions(authArgs); + return runAuthLogin({ + http: dependencies.authHttp, + store: dependencies.credentials, + timeoutMs: options.timeoutMs, + loginHint: parsed.loginHint, + requestedScopes: parsed.scopes, + monotonicNow: dependencies.monotonicNow, + wallNow: dependencies.wallNow, + sleep: dependencies.sleep, + presentVerification: ({ verificationUri, userCode }) => { + dependencies.stderr(options.json + ? `${JSON.stringify({ event: "verification_required", verification_uri: verificationUri, user_code: userCode })}\n` + : `Open ${verificationUri}\nEnter code ${userCode}\n`); + }, + }); + } + exactly(authArgs, 0, `Usage: agentcommunity auth ${subcommand}`); + if (subcommand === "status") return runAuthStatus({ http: dependencies.authHttp, store: dependencies.credentials, timeoutMs: options.timeoutMs, wallNow: dependencies.wallNow }); + if (subcommand === "logout") { + if (options.timeoutSpecified) throw usageError("unknown_option", "auth logout does not accept --timeout because it makes no remote request."); + return runAuthLogout(dependencies.credentials); + } + if (subcommand === "revoke") return runAuthRevoke({ http: dependencies.authHttp, store: dependencies.credentials, timeoutMs: options.timeoutMs }); + throw usageError("unknown_auth_command", `Unknown auth command: ${subcommand}`); + } throw usageError("unknown_command", `Unknown command: ${command}`); } function errorEnvelope(error: CliError): string { + const containsSensitiveValue = /(?:\b(?:access|claim|refresh)?[_ -]?token\b|assertion|claim_attempt|verification[_ -]?uri|\b\d{6}\b|\baca_[A-Za-z0-9_-]+\b|\bclm_[A-Za-z0-9_-]+\b)/i.test(error.message); const body: { error: { code: string; message: string; details?: Record } } = { - error: { code: error.code, message: error.message }, + error: { code: error.code, message: containsSensitiveValue ? "The operation failed without exposing sensitive details." : error.message }, }; - if (error.details !== undefined) body.error.details = error.details; + const retryAfter = error.details?.retry_after_ms; + if (typeof retryAfter === "number" && Number.isSafeInteger(retryAfter) && retryAfter >= 0) { + body.error.details = { retry_after_ms: retryAfter }; + } return `${JSON.stringify(body)}\n`; } @@ -203,9 +290,22 @@ async function readLimitedStdin(maxBytes = BATCH_INPUT_MAX_BYTES): Promise performance.now(), + wallNow: Date.now, + sleep: (milliseconds) => new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds)), readFile: readLimitedFile, readStdin: readLimitedStdin, stdout: (value) => { process.stdout.write(value); }, diff --git a/src/commands/auth.ts b/src/commands/auth.ts new file mode 100644 index 0000000..62af26c --- /dev/null +++ b/src/commands/auth.ts @@ -0,0 +1,268 @@ +import { z } from "zod"; + +import { CliError, type ExitCode } from "../errors.js"; +import type { AuthHttpResponse, AuthHttpTransport } from "../http.js"; +import { credentialRecordSchema } from "../auth/credential-store.js"; +import { AUTH_SCOPES, JWT_BEARER_GRANT, discoverAuthorization } from "../auth/discovery.js"; +import { + DEFAULT_AUTH_DEADLINE_MS, + runServiceAuthLogin, + type CredentialRecord, + type VerificationDetails, +} from "../auth/device-flow.js"; + +export interface CredentialStore { + read(): Promise; + write(value: CredentialRecord): Promise; + replace(expected: CredentialRecord, value: CredentialRecord): Promise; + remove(expected?: CredentialRecord): Promise; +} + +export interface AuthCommandResult { + payload: unknown; + human: string; + exitCode: ExitCode; +} + +interface AuthNetworkOptions { + http: AuthHttpTransport; + store: CredentialStore; + timeoutMs: number; +} + +interface AuthStatusOptions extends AuthNetworkOptions { + wallNow(): number; +} + +interface AuthLoginOptions extends AuthStatusOptions { + loginHint: string; + requestedScopes: Array<(typeof AUTH_SCOPES)[number]>; + deadlineMs?: number; + monotonicNow(): number; + sleep(milliseconds: number): Promise; + presentVerification(value: VerificationDetails): void; +} + +const apiErrorSchema = z.object({ error: z.enum(["invalid_request", "unauthorized", "forbidden", "rate_limited", "service_unavailable"]) }).strict(); +const oauthErrorSchema = z.object({ + error: z.string(), + error_description: z.string(), +}).strict(); +const accountSchema = z.object({ + account: z.object({ id: z.string().uuid(), email: z.string().email(), email_verified: z.literal(true) }).strict(), + authorization: z.object({ + registration_id: z.string().uuid(), + status: z.literal("approved"), + scopes: z.array(z.enum(AUTH_SCOPES)).min(1).max(2), + access_token_expires_at: z.string().datetime({ offset: true }), + delegation_expires_at: z.string().datetime({ offset: true }), + }).strict(), +}).strict(); +const refreshSchema = z.object({ + access_token: z.string().min(1).max(8_192).regex(/^[\u0021-\u007e]+$/), + token_type: z.literal("Bearer"), + expires_in: z.number().int().positive().max(3_600), + scope: z.string().min(1), +}).strict(); + +function protocolError(): CliError { + return new CliError("auth_response_mismatch", "The authorization response did not match the pinned PAGE contract.", 5); +} + +function parseJson(response: AuthHttpResponse): unknown { + const contentType = response.headers["content-type"]; + if (contentType === undefined || !/^application\/json(?:\s*;\s*charset=utf-8)?$/i.test(contentType.trim())) throw protocolError(); + try { + return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(response.body)); + } catch { + throw protocolError(); + } +} + +function exactScopes(value: Array<(typeof AUTH_SCOPES)[number]>, expected: Array<(typeof AUTH_SCOPES)[number]>): boolean { + const canonical = AUTH_SCOPES.filter((scope) => value.includes(scope)); + return new Set(value).size === value.length + && canonical.join(" ") === expected.join(" ") + && value.join(" ") === expected.join(" "); +} + +function parseScopeString(value: string, expected: Array<(typeof AUTH_SCOPES)[number]>): Array<(typeof AUTH_SCOPES)[number]> { + const parsed = z.array(z.enum(AUTH_SCOPES)).safeParse(value.split(" ")); + if (!parsed.success || !exactScopes(parsed.data, expected)) throw protocolError(); + return AUTH_SCOPES.filter((scope) => parsed.data.includes(scope)); +} + +function mapNetworkError(error: unknown): never { + if (error instanceof CliError) throw error; + throw new CliError("network_error", "The Agent Community service could not be reached.", 6); +} + +async function request(http: AuthHttpTransport, value: Parameters[0]): Promise { + try { + return await http.requestAuth(value); + } catch (error) { + return mapNetworkError(error); + } +} + +function unavailable(): never { + throw new CliError("upstream_unavailable", "The Agent Community authorization service is temporarily unavailable.", 6); +} + +function rateLimited(): never { + throw new CliError("rate_limited", "The authorization service rate limit was reached.", 7); +} + +function unauthenticated(reason: "missing_credentials" | "unauthenticated" | "expired_assertion" | "insufficient_scope"): AuthCommandResult { + return { + payload: { authenticated: false, reason }, + human: `Not authenticated (${reason.replaceAll("_", " ")}).`, + exitCode: 4, + }; +} + +export async function runAuthLogin(options: AuthLoginOptions): Promise { + const discovery = await discoverAuthorization(options.http, options.timeoutMs); + const credential = await runServiceAuthLogin({ + http: options.http, + discovery, + loginHint: options.loginHint, + requestedScopes: options.requestedScopes, + timeoutMs: options.timeoutMs, + deadlineMs: options.deadlineMs ?? DEFAULT_AUTH_DEADLINE_MS, + monotonicNow: options.monotonicNow, + wallNow: options.wallNow, + sleep: options.sleep, + presentVerification: options.presentVerification, + store: (value) => options.store.write(value), + }); + return { + payload: { + authenticated: true, + scopes: credential.scopes, + access_token_expires_at: credential.access_token_expires_at, + assertion_expires_at: credential.assertion_expires_at, + }, + human: `Authorization complete. Scopes: ${credential.scopes.join(", ")}.`, + exitCode: 0, + }; +} + +async function refreshAccessToken( + options: AuthStatusOptions, + credential: CredentialRecord, + tokenEndpoint: string, +): Promise { + if (Date.parse(credential.assertion_expires_at) <= options.wallNow()) return unauthenticated("expired_assertion"); + const response = await request(options.http, { + method: "POST", + url: tokenEndpoint, + timeoutMs: options.timeoutMs, + maxBytes: 16_384, + headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: JWT_BEARER_GRANT, + assertion: credential.identity_assertion, + resource: credential.resource, + }).toString(), + }); + if (response.status >= 500) unavailable(); + if (response.status === 429) rateLimited(); + if (response.status === 400 || response.status === 401) { + const parsed = oauthErrorSchema.safeParse(parseJson(response)); + if (!parsed.success) throw protocolError(); + if (parsed.data.error === "invalid_grant" && parsed.data.error_description === "The authorization grant is invalid") { + return unauthenticated("unauthenticated"); + } + throw protocolError(); + } + if (response.status !== 200) throw protocolError(); + const parsed = refreshSchema.safeParse(parseJson(response)); + if (!parsed.success) throw protocolError(); + const scopes = parseScopeString(parsed.data.scope, credential.scopes); + const replacement: CredentialRecord = { + ...credential, + scopes, + access_token: parsed.data.access_token, + access_token_expires_at: new Date(options.wallNow() + parsed.data.expires_in * 1_000).toISOString(), + }; + if (!credentialRecordSchema.safeParse(replacement).success) throw protocolError(); + if (!await options.store.replace(credential, replacement)) { + throw new CliError("credential_changed", "The local credential changed during refresh; no credential was overwritten.", 4); + } + return replacement; +} + +export async function runAuthStatus(options: AuthStatusOptions): Promise { + let credential = await options.store.read(); + if (credential === null) return unauthenticated("missing_credentials"); + const discovery = await discoverAuthorization(options.http, options.timeoutMs); + if (credential.issuer !== discovery.issuer || credential.resource !== discovery.resource || !credential.scopes.every((scope) => discovery.scopes.includes(scope))) { + throw new CliError("unsafe_credential_store", "The local credential store is unsafe.", 4); + } + if (Date.parse(credential.access_token_expires_at) <= options.wallNow()) { + const refreshed = await refreshAccessToken(options, credential, discovery.tokenEndpoint); + if (!("access_token" in refreshed)) return refreshed; + credential = refreshed; + } + const response = await request(options.http, { + method: "GET", + url: `${discovery.resource}/v1/agent/account`, + timeoutMs: options.timeoutMs, + maxBytes: 32_768, + headers: { Accept: "application/json", Authorization: `Bearer ${credential.access_token}` }, + }); + if (response.status >= 500) unavailable(); + if (response.status === 429) rateLimited(); + if (response.status === 401 || response.status === 403) { + const parsed = apiErrorSchema.safeParse(parseJson(response)); + if (!parsed.success || (response.status === 401 && parsed.data.error !== "unauthorized") || (response.status === 403 && parsed.data.error !== "forbidden")) { + throw protocolError(); + } + return unauthenticated(response.status === 401 ? "unauthenticated" : "insufficient_scope"); + } + if (response.status !== 200) throw protocolError(); + const parsed = accountSchema.safeParse(parseJson(response)); + if (!parsed.success || !exactScopes(parsed.data.authorization.scopes, credential.scopes)) throw protocolError(); + return { + payload: { authenticated: true, ...parsed.data }, + human: `Authenticated as ${parsed.data.account.email}. Scopes: ${parsed.data.authorization.scopes.join(", ")}.`, + exitCode: 0, + }; +} + +export async function runAuthLogout(store: CredentialStore): Promise { + const removed = await store.remove(); + return { + payload: { logged_out: true, credential_removed: removed }, + human: removed ? "Local credentials removed." : "No local credentials were present.", + exitCode: 0, + }; +} + +export async function runAuthRevoke(options: AuthNetworkOptions): Promise { + const credential = await options.store.read(); + if (credential === null) return { payload: { revoked: false, reason: "missing_credentials" }, human: "No local credential is available to revoke.", exitCode: 4 }; + const discovery = await discoverAuthorization(options.http, options.timeoutMs); + if (credential.issuer !== discovery.issuer || credential.resource !== discovery.resource) { + throw new CliError("unsafe_credential_store", "The local credential store is unsafe.", 4); + } + const response = await request(options.http, { + method: "POST", + url: discovery.revocationEndpoint, + timeoutMs: options.timeoutMs, + maxBytes: 8_192, + headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ token: credential.access_token, token_type_hint: "access_token" }).toString(), + }); + if (response.status >= 500) unavailable(); + if (response.status === 429) rateLimited(); + if (response.status !== 200 || response.body.byteLength !== 0) throw protocolError(); + const removed = await options.store.remove(credential); + if (!removed) throw new CliError("credential_changed", "The local credential changed after revocation and was preserved.", 4); + return { + payload: { revoked: true, credential_removed: true }, + human: "The current access token revocation request was accepted and its matching local credential was removed.", + exitCode: 0, + }; +} diff --git a/src/http.ts b/src/http.ts index d11ae97..a2f733c 100644 --- a/src/http.ts +++ b/src/http.ts @@ -15,6 +15,25 @@ export interface HttpTransport { requestJson(request: JsonRequest): Promise; } +export interface AuthHttpRequest { + method: "GET" | "POST"; + url: string; + timeoutMs: number; + maxBytes: number; + headers: Record; + body?: string; +} + +export interface AuthHttpResponse { + status: number; + headers: Record; + body: Uint8Array; +} + +export interface AuthHttpTransport { + requestAuth(request: AuthHttpRequest): Promise; +} + const MAX_RETRY_AFTER_MS = 300_000; function retryAfterDetails(value: string | null, now: number): Record | undefined { @@ -63,12 +82,54 @@ async function readBounded(response: Response, maxBytes: number): Promise number = Date.now, ) {} + async requestAuth(request: AuthHttpRequest): Promise { + let url: URL; + try { + url = new URL(request.url); + } catch { + throw new CliError("unsafe_auth_endpoint", "The authorization endpoint is not allowed.", 5); + } + if ( + url.protocol !== "https:" + || url.origin !== AGENT_COMMUNITY_ORIGIN + || url.username !== "" + || url.password !== "" + || url.hash !== "" + ) { + throw new CliError("unsafe_auth_endpoint", "The authorization endpoint is not allowed.", 5); + } + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), request.timeoutMs); + try { + const init: RequestInit = { + method: request.method, + headers: request.headers, + redirect: "manual", + signal: controller.signal, + }; + if (request.body !== undefined) init.body = request.body; + const response = await this.fetchImpl(url.href, init); + const body = await readBounded(response, request.maxBytes); + const headers: Record = {}; + response.headers.forEach((value, name) => { headers[name.toLowerCase()] = value; }); + return { status: response.status, headers, body }; + } catch (error) { + if (error instanceof CliError) throw error; + if (error instanceof DOMException && error.name === "AbortError") { + throw new CliError("timeout", "The request timed out.", 6); + } + throw new CliError("network_error", "The Agent Community service could not be reached.", 6); + } finally { + clearTimeout(timer); + } + } + async requestJson(request: JsonRequest): Promise { if (!request.path.startsWith("/") || request.path.startsWith("//")) { throw new CliError("invalid_path", "The request path is invalid.", 2); From a2c5b13c417302f2e1d4934306ee8d66fb64993d Mon Sep 17 00:00:00 2001 From: nembal Date: Sun, 2 Aug 2026 04:04:04 +0700 Subject: [PATCH 4/4] fix(auth): harden CLI credential lifecycle --- src/auth/__tests__/auth-commands.test.ts | 13 ++ src/auth/__tests__/credential-store.test.ts | 190 +++++++++++++++++++- src/auth/__tests__/device-flow.test.ts | 30 +++- src/auth/credential-store.ts | 83 ++++++++- src/auth/device-flow.ts | 6 +- src/commands/auth.ts | 2 +- 6 files changed, 314 insertions(+), 10 deletions(-) diff --git a/src/auth/__tests__/auth-commands.test.ts b/src/auth/__tests__/auth-commands.test.ts index 87fda3d..288b411 100644 --- a/src/auth/__tests__/auth-commands.test.ts +++ b/src/auth/__tests__/auth-commands.test.ts @@ -177,6 +177,19 @@ describe("auth status, logout, and RFC 7009 revoke", () => { expect(local.store.remove).toHaveBeenCalledWith(credential); }); + test.each([ + ["a non-empty JSON body", json(200, { ignored: true })], + ["a non-JSON invalid-UTF8 body", { status: 200, headers: { "content-type": "application/octet-stream" }, body: Uint8Array.from([0xff, 0xfe, 0xfd]) }], + ])("accepts bounded RFC 7009 HTTP 200 with %s and ignores the body", async (_label, accepted) => { + const network = httpHarness([...discoveryResponses(), accepted]); + const local = storeHarness(); + + await expect(runAuthRevoke({ http: network.http, store: local.store, timeoutMs: 10_000 })) + .resolves.toMatchObject({ exitCode: 0, payload: { revoked: true, credential_removed: true } }); + expect(local.store.remove).toHaveBeenCalledWith(credential); + expect(local.current()).toBeNull(); + }); + test.each([ [json(503, { error: "temporarily_unavailable" }), 6], [json(400, { error: "invalid_request", error_description: "The request is malformed" }), 5], diff --git a/src/auth/__tests__/credential-store.test.ts b/src/auth/__tests__/credential-store.test.ts index e8a045c..f4c5eac 100644 --- a/src/auth/__tests__/credential-store.test.ts +++ b/src/auth/__tests__/credential-store.test.ts @@ -1,6 +1,5 @@ import * as fs from "node:fs/promises"; import { constants } from "node:fs"; -import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, test, vi } from "vitest"; @@ -28,7 +27,7 @@ const credential: CredentialRecord = { const roots: Array = []; async function root(): Promise { - const value = await fs.mkdtemp(join(tmpdir(), "agentcommunity-store-test-")); + const value = await fs.mkdtemp(join(process.cwd(), ".agentcommunity-store-test-")); roots.push(value); return value; } @@ -54,6 +53,14 @@ function store(homeDirectory: string, overrides: Partial { test("selects exact macOS and Linux paths and rejects unsupported or relative roots", () => { expect(credentialPath({ platform: "darwin", homeDirectory: "/Users/fixture" })) @@ -84,6 +91,34 @@ describe("POSIX credential path and safe storage", () => { expect(await fs.readdir(join(home, ".config", "agentcommunity"))).toEqual(["credentials.json"]); }); + test("supports a normal macOS home path with fully validated ancestry", async () => { + const home = await root(); + const targetStore = store(home, { platform: "darwin" }); + + await targetStore.write(credential); + + expect(await targetStore.read()).toEqual(credential); + expect(credentialPath({ platform: "darwin", homeDirectory: home })) + .toBe(join(home, "Library", "Application Support", "agentcommunity", "credentials.json")); + }); + + test.each(["home", "external XDG root"])('rejects a safe-looking %s below an unsafe ancestor', async (kind) => { + const fixture = await root(); + const configuredHome = kind === "home" ? undefined : join(fixture, "home"); + if (configuredHome !== undefined) await fs.mkdir(configuredHome, { mode: 0o700 }); + const unsafeAncestor = join(fixture, `unsafe-${kind.replaceAll(" ", "-")}`); + await fs.mkdir(unsafeAncestor, { mode: 0o700 }); + const nestedRoot = join(unsafeAncestor, kind === "home" ? "home" : "xdg"); + await fs.mkdir(nestedRoot, { mode: 0o700 }); + await fs.chmod(unsafeAncestor, 0o777); + const targetStore = kind === "home" + ? store(nestedRoot) + : store(configuredHome ?? fixture, { xdgConfigHome: nestedRoot }); + + await expect(targetStore.write(credential)) + .rejects.toMatchObject({ exitCode: 4, code: "unsafe_credential_store" }); + }); + test.each([ ["symlink", async (path: string, home: string) => fs.symlink(join(home, "outside"), path)], ["hardlink", async (path: string, home: string) => { @@ -201,6 +236,121 @@ describe("POSIX credential path and safe storage", () => { expect(await fs.readdir(join(home, ".config", "agentcommunity"))).toEqual(["credentials.json"]); }); + test("cleans valid crash-orphan task temp files on the next write and logout", async () => { + const home = await root(); + const targetStore = store(home); + await targetStore.write(credential); + const firstOrphan = orphanPath(home); + await fs.writeFile(firstOrphan, "orphan-secret-fixture-only", { mode: 0o600, flag: "wx" }); + + const replacement = { ...credential, access_token: "replacement" }; + await targetStore.write(replacement); + await expect(fs.lstat(firstOrphan)).rejects.toMatchObject({ code: "ENOENT" }); + expect(await targetStore.read()).toEqual(replacement); + + const secondOrphan = orphanPath(home, "8888-second-crash"); + await fs.writeFile(secondOrphan, "second-orphan-secret-fixture-only", { mode: 0o600, flag: "wx" }); + expect(await targetStore.remove()).toBe(true); + await expect(fs.lstat(secondOrphan)).rejects.toMatchObject({ code: "ENOENT" }); + expect(await fs.readdir(storeDirectory(home))).toEqual([]); + }); + + test.each([ + ["symlink", async (path: string, home: string) => fs.symlink(join(home, "outside-orphan"), path)], + ["hardlink", async (path: string, home: string) => { + const outside = join(home, "outside-orphan"); + await fs.writeFile(outside, "orphan-secret-fixture-only", { mode: 0o600 }); + await fs.link(outside, path); + }], + ["wrong mode", async (path: string) => fs.writeFile(path, "orphan-secret-fixture-only", { mode: 0o640 })], + ["nonregular directory", async (path: string) => fs.mkdir(path, { mode: 0o700 })], + ["oversized file", async (path: string) => fs.writeFile(path, Buffer.alloc(32_769), { mode: 0o600 })], + ])("refuses an exact-pattern crash orphan that is a %s", async (_label, createOrphan) => { + const home = await root(); + const targetStore = store(home); + await targetStore.write(credential); + const orphan = orphanPath(home); + await createOrphan(orphan, home); + + await expect(targetStore.write({ ...credential, access_token: "replacement" })) + .rejects.toMatchObject({ exitCode: 4, code: "unsafe_credential_store" }); + expect(await fs.lstat(orphan)).toBeDefined(); + }); + + test("refuses an exact-pattern crash orphan with the wrong owner", async () => { + const home = await root(); + const targetStore = store(home); + await targetStore.write(credential); + const orphan = orphanPath(home); + await fs.writeFile(orphan, "orphan-secret-fixture-only", { mode: 0o600, flag: "wx" }); + const wrongOwnerFs: CredentialFileSystem = { + ...fs, + open: async (path, flags, mode) => { + const handle = await fs.open(path, flags, mode); + if (String(path) !== orphan) return handle; + return new Proxy(handle, { + get(target, property, receiver) { + if (property === "stat") return async () => { + const stat = await target.stat(); + return new Proxy(stat, { + get(statTarget, statProperty, statReceiver) { + if (statProperty === "uid") return uid + 1; + const value = Reflect.get(statTarget, statProperty, statReceiver) as unknown; + return typeof value === "function" ? value.bind(statTarget) : value; + }, + }); + }; + const value = Reflect.get(target, property, receiver) as unknown; + return typeof value === "function" ? value.bind(target) : value; + }, + }); + }, + }; + + await expect(store(home, { fs: wrongOwnerFs }).write({ ...credential, access_token: "replacement" })) + .rejects.toMatchObject({ exitCode: 4, code: "unsafe_credential_store" }); + expect(await fs.lstat(orphan)).toBeDefined(); + }); + + test("refuses an exact-pattern crash orphan whose inode changes between lstat and open", async () => { + const home = await root(); + const targetStore = store(home); + await targetStore.write(credential); + const orphan = orphanPath(home); + const other = join(home, "different-safe-file"); + await fs.writeFile(orphan, "orphan-secret-fixture-only", { mode: 0o600, flag: "wx" }); + await fs.writeFile(other, "different-file", { mode: 0o600, flag: "wx" }); + const changedInodeFs: CredentialFileSystem = { + ...fs, + open: async (path, flags, mode) => fs.open(String(path) === orphan ? other : path, flags, mode), + }; + + await expect(store(home, { fs: changedInodeFs }).write({ ...credential, access_token: "replacement" })) + .rejects.toMatchObject({ exitCode: 4, code: "unsafe_credential_store" }); + expect(await fs.lstat(orphan)).toBeDefined(); + }); + + test("cleans only exact task temp names and preserves unknown neighboring entries", async () => { + const home = await root(); + const targetStore = store(home); + await targetStore.write(credential); + const exact = orphanPath(home); + const unknownNames = [ + ".credentials.json.tmp-not-a-task-temp", + ".credentials.json.tmp-9999-has.dot", + "credentials.json.backup", + ]; + await fs.writeFile(exact, "orphan-secret-fixture-only", { mode: 0o600, flag: "wx" }); + for (const name of unknownNames) { + await fs.writeFile(join(storeDirectory(home), name), "preserve-me", { mode: 0o600, flag: "wx" }); + } + + await targetStore.write({ ...credential, access_token: "replacement" }); + + await expect(fs.lstat(exact)).rejects.toMatchObject({ code: "ENOENT" }); + for (const name of unknownNames) expect(await fs.readFile(join(storeDirectory(home), name), "utf8")).toBe("preserve-me"); + }); + test("logout is remote-free store removal, safe, and idempotent", async () => { const home = await root(); const targetStore = store(home); @@ -211,6 +361,42 @@ describe("POSIX credential path and safe storage", () => { expect(await targetStore.read()).toBeNull(); }); + test("unconditional logout removes the valid credential that appears while it waits for the lock", async () => { + const home = await root(); + const targetStore = store(home); + await targetStore.write(credential); + const directory = storeDirectory(home); + const path = credentialPath({ platform: "linux", homeDirectory: home }); + const lockPath = `${path}.lock`; + await fs.writeFile(lockPath, `${JSON.stringify({ pid: 9999, created_at_ms: Date.now() })}\n`, { mode: 0o600, flag: "wx" }); + let released = false; + const changed = { ...credential, access_token: "new-valid-credential" }; + const racingStore = store(home, { + sleep: async () => { + if (released) return; + released = true; + const replacementPath = join(directory, ".race-replacement"); + await fs.writeFile(replacementPath, `${JSON.stringify(changed)}\n`, { mode: 0o600, flag: "wx" }); + await fs.rename(replacementPath, path); + await fs.unlink(lockPath); + }, + }); + + expect(await racingStore.remove()).toBe(true); + expect(await racingStore.read()).toBeNull(); + }); + + test("conditional removal preserves a credential changed by another writer", async () => { + const home = await root(); + const targetStore = store(home); + await targetStore.write(credential); + const changed = { ...credential, access_token: "other-writer" }; + await targetStore.write(changed); + + expect(await targetStore.remove(credential)).toBe(false); + expect(await targetStore.read()).toEqual(changed); + }); + test("conditional replacement preserves a credential changed by another writer", async () => { const home = await root(); const targetStore = store(home); diff --git a/src/auth/__tests__/device-flow.test.ts b/src/auth/__tests__/device-flow.test.ts index bc584c8..c52da62 100644 --- a/src/auth/__tests__/device-flow.test.ts +++ b/src/auth/__tests__/device-flow.test.ts @@ -48,7 +48,9 @@ function errorResponse(error: string, description: string): AuthHttpResponse { return response(400, { error, error_description: description }); } -function harness(outcomes: Array, deadlineMs = 900_000) { +type Outcome = AuthHttpResponse | Error | { response: AuthHttpResponse; advanceMs: number }; + +function harness(outcomes: Array, deadlineMs = 900_000) { const requests: Array = []; const events: Array = []; const stored: Array = []; @@ -61,6 +63,10 @@ function harness(outcomes: Array, deadlineMs = 900_000 const outcome = outcomes.shift(); if (outcome instanceof Error) throw outcome; if (outcome === undefined) throw new Error("unexpected request"); + if ("response" in outcome) { + monotonic += outcome.advanceMs; + return outcome.response; + } return outcome; }), }; @@ -172,6 +178,28 @@ describe("WorkOS service_auth login", () => { expect(flow.stored).toEqual([]); }); + test("bounds each poll timeout to remaining ceremony time and stores a response just before the deadline", async () => { + const flow = harness([ + response(200, startBody), + { response: response(200, successBody), advanceMs: 4_999 }, + ], 10_000); + + await expect(flow.promise).resolves.toMatchObject({ access_token: accessToken }); + expect(flow.requests[1]?.timeoutMs).toBe(5_000); + expect(flow.stored).toHaveLength(1); + }); + + test("rejects a successful poll that returns exactly at the ceremony deadline without parsing or storing it", async () => { + const flow = harness([ + response(200, startBody), + { response: response(200, successBody), advanceMs: 5_000 }, + ], 10_000); + + await expect(flow.promise).rejects.toMatchObject({ exitCode: 4, code: "authorization_timeout" }); + expect(flow.requests[1]?.timeoutMs).toBe(5_000); + expect(flow.stored).toEqual([]); + }); + test("rejects invalid deadline bounds before any request", async () => { for (const deadlineMs of [0, 1_800_001]) { const flow = harness([], deadlineMs); diff --git a/src/auth/credential-store.ts b/src/auth/credential-store.ts index ef08c91..fe56424 100644 --- a/src/auth/credential-store.ts +++ b/src/auth/credential-store.ts @@ -14,6 +14,7 @@ const STORE_DIRECTORY = "agentcommunity"; const DIRECTORY_MODE = 0o700; const FILE_MODE = 0o600; const MAX_CREDENTIAL_BYTES = 32_768; +const TASK_TEMP_FILE_PATTERN = /^\.credentials\.json\.tmp-(?:0|[1-9]\d{0,19})-[A-Za-z0-9_-]{1,64}$/; const DEFAULT_LOCK_TIMEOUT_MS = 2_000; const DEFAULT_STALE_LOCK_MS = 5 * 60 * 1_000; const LOCK_POLL_MS = 50; @@ -176,18 +177,17 @@ export class PosixCredentialStore { async remove(expected?: CredentialRecord): Promise { const directory = await this.ensureDirectory(false); if (directory === null) return false; - const before = await this.safeLstat(this.path); - if (before === null) return false; - this.requireSafeFile(before); return this.withLock(directory, async () => { const current = await this.safeLstat(this.path); if (current === null) return false; this.requireSafeFile(current); - if (current.dev !== before.dev || current.ino !== before.ino) return false; if (expected !== undefined) { const record = await this.readCredentialFile(); if (record === null || JSON.stringify(record) !== JSON.stringify(expected)) return false; } + const final = await this.safeLstat(this.path); + if (final === null || final.dev !== current.dev || final.ino !== current.ino) return false; + this.requireSafeFile(final); await this.fs.unlink(this.path); await this.syncDirectory(directory); return true; @@ -216,9 +216,43 @@ export class PosixCredentialStore { } } + private requireSafeAncestor(stat: Awaited>): void { + if (!stat.isDirectory() || stat.isSymbolicLink() || (Number(stat.mode) & 0o022) !== 0) throw storeError(); + } + + private async validateAncestorChain(path: string): Promise { + const ancestors: Array = []; + let current = requireAbsoluteRoot(path); + while (true) { + ancestors.unshift(current); + const parent = dirname(current); + if (parent === current) break; + current = parent; + } + for (const ancestor of ancestors) { + const before = await this.safeLstat(ancestor); + if (before === null) return; + this.requireSafeAncestor(before); + let handle: Awaited>; + try { + handle = await this.fs.open(ancestor, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW); + } catch { + throw storeError(); + } + try { + const opened = await handle.stat(); + this.requireSafeAncestor(opened); + if (opened.dev !== before.dev || opened.ino !== before.ino) throw storeError(); + } finally { + await handle.close(); + } + } + } + private async ensureDirectory(create: boolean): Promise { const plan = pathPlan(this.options); let current = plan.base; + await this.validateAncestorChain(current); let baseStat = await this.safeLstat(current); if (baseStat === null) { if (!create || current !== requireAbsoluteRoot(this.options.xdgConfigHome ?? this.options.homeDirectory)) return null; @@ -232,6 +266,7 @@ export class PosixCredentialStore { if (errorCode(error) !== "EEXIST") throw storeError(); } baseStat = await this.safeLstat(current); + await this.validateAncestorChain(current); } if (baseStat === null) return null; this.requireSafeDirectory(baseStat, false); @@ -251,6 +286,7 @@ export class PosixCredentialStore { this.requireSafeDirectory(stat, index === plan.segments.length - 1); } if (current !== dirname(this.path)) throw storeError(); + await this.validateAncestorChain(current); return current; } @@ -331,6 +367,7 @@ export class PosixCredentialStore { } } try { + await this.cleanupTaskTempFiles(directory); return await operation(); } finally { await lockHandle.close().catch(() => undefined); @@ -344,6 +381,44 @@ export class PosixCredentialStore { } } + private async cleanupTaskTempFiles(directory: string): Promise { + let names: Array; + try { + names = await this.fs.readdir(directory); + } catch { + throw storeError(); + } + let removed = false; + for (const name of names) { + if (!TASK_TEMP_FILE_PATTERN.test(name)) continue; + const path = join(directory, name); + const before = await this.safeLstat(path); + if (before === null) continue; + this.requireSafeFile(before); + if (before.size > MAX_CREDENTIAL_BYTES) throw storeError(); + let handle: Awaited>; + try { + handle = await this.fs.open(path, constants.O_RDONLY | constants.O_NOFOLLOW); + } catch { + throw storeError(); + } + try { + const opened = await handle.stat(); + this.requireSafeFile(opened); + if (opened.dev !== before.dev || opened.ino !== before.ino || opened.size > MAX_CREDENTIAL_BYTES) throw storeError(); + const final = await this.safeLstat(path); + if (final === null || final.dev !== opened.dev || final.ino !== opened.ino) throw storeError(); + this.requireSafeFile(final); + if (final.size > MAX_CREDENTIAL_BYTES) throw storeError(); + await this.fs.unlink(path); + removed = true; + } finally { + await handle.close(); + } + } + if (removed) await this.syncDirectory(directory); + } + private async considerStaleLock(lockPath: string): Promise { const before = await this.safeLstat(lockPath); if (before === null) return; diff --git a/src/auth/device-flow.ts b/src/auth/device-flow.ts index cfc36dc..1c87a8c 100644 --- a/src/auth/device-flow.ts +++ b/src/auth/device-flow.ts @@ -204,13 +204,14 @@ export async function runServiceAuthLogin(options: ServiceAuthLoginOptions): Pro while (true) { if (options.monotonicNow() + intervalMs > deadlineAt) throw deadlineError(); await options.sleep(intervalMs); - if (options.monotonicNow() >= deadlineAt) throw deadlineError(); + const remainingMs = deadlineAt - options.monotonicNow(); + if (remainingMs <= 0) throw deadlineError(); let pollResponse: AuthHttpResponse; try { pollResponse = await options.http.requestAuth({ method: "POST", url: options.discovery.tokenEndpoint, - timeoutMs: options.timeoutMs, + timeoutMs: Math.min(options.timeoutMs, remainingMs), maxBytes: 16_384, headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: CLAIM_GRANT, claim_token: start.data.claim_token }).toString(), @@ -220,6 +221,7 @@ export async function runServiceAuthLogin(options: ServiceAuthLoginOptions): Pro lastPollWasUnavailable = true; continue; } + if (options.monotonicNow() >= deadlineAt) throw deadlineError(); if (pollResponse.status >= 500) { lastPollWasUnavailable = true; continue; diff --git a/src/commands/auth.ts b/src/commands/auth.ts index 62af26c..39ce1d5 100644 --- a/src/commands/auth.ts +++ b/src/commands/auth.ts @@ -257,7 +257,7 @@ export async function runAuthRevoke(options: AuthNetworkOptions): Promise= 500) unavailable(); if (response.status === 429) rateLimited(); - if (response.status !== 200 || response.body.byteLength !== 0) throw protocolError(); + if (response.status !== 200) throw protocolError(); const removed = await options.store.remove(credential); if (!removed) throw new CliError("credential_changed", "The local credential changed after revocation and was preserved.", 4); return {