From 3b2bf5b712bdd9ec8cc4bc618b233e443e508cd9 Mon Sep 17 00:00:00 2001 From: I4cDeath Date: Mon, 27 Apr 2026 20:24:56 +0900 Subject: [PATCH 01/16] =?UTF-8?q?feat:=20v0.11.7=20=E2=80=94=20Glama=20TDQ?= =?UTF-8?q?S=20overhaul=20for=20all=2044=20MCP=20tools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 44 tools now ship a 3-sentence description hitting the six Glama Tool Definition Quality Score dimensions (purpose, when-to-use vs. siblings, side-effects/audit/network, parameter semantics, conciseness, returns). Common parameter schemas (`scope`, `projectPath`, `env`, `teamId`, `orgId`) and per-tool Zod `.describe()` strings now include formats, defaults, and concrete examples to lift the per-tool minimum score that dominates the server-level grade (60% mean / 40% min). - src/mcp/tools/_shared.ts: enriched commonSchemas describe() strings - src/mcp/tools/{secrets,project,tunnel,teleport,audit,validation,hooks, tooling,agent,policy}.ts: 44 tool descriptions rewritten + parameter describe() strings tightened - README.md: MCP tables resynced with new one-liners - CHANGELOG.md: 0.11.7 entry - package.json / server.json / plugin.json / marketplace.json: 0.11.7 No runtime / MCP wire-format changes — agents will see longer, clearer tool descriptions on next tools/list refresh; tool names, parameters, and return shapes are unchanged. Test plan: - pnpm run typecheck ✓ - pnpm run lint ✓ (max-warnings 0) - pnpm run build ✓ - pnpm run test:ci ✓ (24 files / 164 tests) Made-with: Cursor --- .cursor-plugin/marketplace.json | 2 +- CHANGELOG.md | 11 ++ README.md | 88 ++++----- cursor-plugin/.cursor-plugin/plugin.json | 2 +- package.json | 2 +- server.json | 4 +- src/mcp/tools/_shared.ts | 20 +- src/mcp/tools/agent.ts | 39 +++- src/mcp/tools/audit.ts | 74 ++++++-- src/mcp/tools/hooks.ts | 82 ++++++-- src/mcp/tools/policy.ts | 31 ++- src/mcp/tools/project.ts | 24 ++- src/mcp/tools/secrets.ts | 230 ++++++++++++++++++----- src/mcp/tools/teleport.ts | 38 +++- src/mcp/tools/tooling.ts | 91 +++++++-- src/mcp/tools/tunnel.ts | 52 ++++- src/mcp/tools/validation.ts | 45 ++++- 17 files changed, 651 insertions(+), 184 deletions(-) diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index f50eebe..2a6022b 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -12,7 +12,7 @@ "name": "qring", "source": "cursor-plugin", "description": "Quantum keyring for AI agents — manage secrets, scan for leaks, rotate keys, and enforce policy directly from Cursor.", - "version": "0.11.5", + "version": "0.11.7", "keywords": [ "secrets", "keyring", diff --git a/CHANGELOG.md b/CHANGELOG.md index f03d84a..0b95921 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.11.7] — 2026-04-27 + +### Changed +- **MCP tool descriptions overhauled for Glama TDQS** — every one of the 44 MCP tools now ships a 3-sentence description hitting all six [Tool Definition Quality Score](https://glama.ai/blog/2026-04-03-tool-definition-quality-score-tdqs) dimensions (purpose, when-to-use vs. siblings, side effects/audit/network, parameter semantics, conciseness, returns). Common parameter schemas (`scope`, `projectPath`, `env`, `teamId`, `orgId`) and per-tool Zod `.describe()` strings now include formats, defaults, and concrete examples to lift the per-tool minimum score (which dominates the server-level grade at 60% mean / 40% min). README MCP tables resynced with the new one-liners. +- **`feature-docs-sync.mdc` rule rewritten** — drops stale `web/components/...` globs (the marketing site was extracted to its own repo in 0.11.5) and replaces them with an explicit `quantum_ring` ↔ `qring.i4c.studio` cross-repo file mapping covering `lib/data/{features,mcp-tools,cli-commands,cli-reference,changelog,version}.ts`. +- **`release-process.mdc` rule** — `Downstream Sync` table now lists the marketing site, Cursor plugin, Kiro plugin, and Claude Code plugin alongside Glama, with explicit commands and a note that the marketing-site sync is not enforced by `quantum_ring` CI. + +### Notes +- No runtime / MCP wire-format changes — this is a documentation-quality release. Existing agents and integrations will see longer, clearer tool descriptions and richer parameter help when they next refresh `tools/list`, but tool names, parameter names, and return shapes are unchanged. +- After publish, trigger a Glama re-sync from the admin panel so the new descriptions feed the next TDQS scoring run. + ## [0.11.5] — 2026-04-27 ### Added diff --git a/README.md b/README.md index c874332..759e11a 100644 --- a/README.md +++ b/README.md @@ -663,95 +663,95 @@ q-ring includes a full MCP server with 44 tools for AI agent integration. | Tool | Description | |------|-------------| -| `get_secret` | Retrieve with superposition collapse + observer logging | -| `list_secrets` | List keys with quantum metadata, filterable by tag/expiry/pattern | -| `set_secret` | Store with optional TTL, env state, tags, rotation format | -| `delete_secret` | Remove a secret | -| `has_secret` | Boolean check (respects decay) | -| `export_secrets` | Export as .env/JSON with optional key and tag filters | -| `import_dotenv` | Parse and import secrets from .env content | -| `check_project` | Validate project secrets against `.q-ring.json` manifest | -| `env_generate` | Generate .env content from the project manifest | +| `get_secret` | Read a secret value (collapses superposition, audits the read) | +| `list_secrets` | List keys + metadata in scope (values never exposed); filter by tag, expiry, glob | +| `set_secret` | Create or overwrite a single secret with optional TTL, per-env state, tags, rotation format | +| `delete_secret` | Permanently remove a secret value (not undoable from q-ring) | +| `has_secret` | Boolean existence check that respects decay (no audit read) | +| `export_secrets` | Render multiple secrets as `.env` or JSON for one-off export | +| `import_dotenv` | Parse `.env` text and bulk-store every key/value pair | +| `check_project` | Compare `.q-ring.json` manifest against the keyring for missing/expired/stale keys | +| `env_generate` | Render a complete `.env` body from the project manifest, with warnings for gaps | ### Quantum Tools | Tool | Description | |------|-------------| -| `inspect_secret` | Full quantum state (states, decay, entanglement, access count) | -| `detect_environment` | Wavefunction collapse — detect current env context | -| `generate_secret` | Quantum noise — generate and optionally save secrets | -| `entangle_secrets` | Link two secrets for synchronized rotation | -| `disentangle_secrets` | Remove entanglement between two secrets | +| `inspect_secret` | Show metadata for one key (states, decay, entanglement, access count) without revealing the value | +| `detect_environment` | Resolve which env slug should drive superposition collapse for the current context | +| `generate_secret` | Generate a CSPRNG-backed value in a chosen format and optionally store it | +| `entangle_secrets` | Link two keys so future writes/rotations propagate the same value | +| `disentangle_secrets` | Break the sync link between two keys (does not delete values) | ### Tunneling Tools | Tool | Description | |------|-------------| -| `tunnel_create` | Create ephemeral in-memory secret | -| `tunnel_read` | Read (may self-destruct) | -| `tunnel_list` | List active tunnels | -| `tunnel_destroy` | Immediately destroy | +| `tunnel_create` | Stash a value in process memory and return an opaque ID (never touches disk) | +| `tunnel_read` | Fetch a tunneled value by ID — may self-destruct on read | +| `tunnel_list` | Enumerate active tunnels with remaining read budget and TTL (IDs only) | +| `tunnel_destroy` | Immediately remove a tunnel from memory before its TTL/reads run out | ### Teleportation Tools | Tool | Description | |------|-------------| -| `teleport_pack` | Encrypt secrets into a portable bundle | -| `teleport_unpack` | Decrypt and import a bundle | +| `teleport_pack` | Encrypt selected secrets into a passphrase-protected AES-256-GCM bundle | +| `teleport_unpack` | Decrypt a teleport bundle and import each secret (with optional dry-run) | ### Validation Tools | Tool | Description | |------|-------------| -| `validate_secret` | Test if a secret is valid with its target service (OpenAI, Stripe, GitHub, etc.) | -| `list_providers` | List all available validation providers | +| `validate_secret` | Hit the upstream service (OpenAI/Stripe/GitHub/AWS/HTTP) to confirm a single key is still live | +| `list_providers` | Enumerate built-in validation providers and their auto-detect prefixes | ### Hook Tools | Tool | Description | |------|-------------| -| `register_hook` | Register a shell/HTTP/signal callback on secret changes | -| `list_hooks` | List all registered hooks with match criteria and status | -| `remove_hook` | Remove a registered hook by ID | +| `register_hook` | Register a shell/HTTP/signal side-effect that fires on write/delete/rotate | +| `list_hooks` | Show every registered hook with match criteria, type, and enabled flag | +| `remove_hook` | Detach a single hook by ID without touching any secrets | ### Execution & Scanning Tools | Tool | Description | |------|-------------| -| `exec_with_secrets` | Run a shell command securely with secrets injected, auto-redacted output, and exec profile enforcement | -| `scan_codebase_for_secrets` | Scan a directory for hardcoded secrets using regex heuristics and entropy analysis | -| `lint_files` | Lint specific files for hardcoded secrets with optional auto-fix | +| `exec_with_secrets` | Run a child command with secrets injected as env vars and any leaked values redacted from output | +| `scan_codebase_for_secrets` | Walk a directory tree and flag hardcoded secrets via regex + entropy heuristics | +| `lint_files` | Inspect a specific file list for hardcoded secrets with optional auto-fix to `process.env.KEY` | ### AI Agent Tools | Tool | Description | |------|-------------| -| `get_project_context` | Safe, redacted overview of project secrets, environment, manifest, and activity | -| `agent_remember` | Store a key-value pair in encrypted agent memory (persists across sessions) | -| `agent_recall` | Retrieve from agent memory, or list all stored keys | -| `agent_forget` | Delete a key from agent memory | -| `analyze_secrets` | Usage analytics: most accessed, stale, unused, and rotation recommendations | +| `get_project_context` | Single redacted snapshot of secrets, env, manifest, hooks, and recent audit activity | +| `agent_remember` | Persist a non-secret note in encrypted agent memory across sessions | +| `agent_recall` | Read a memory value, or list every stored key when no key is supplied | +| `agent_forget` | Permanently delete a key from agent memory | +| `analyze_secrets` | Usage profile: most-accessed, stale, never-accessed, no-rotation candidates | ### Observer & Health Tools | Tool | Description | |------|-------------| -| `audit_log` | Query access history | -| `detect_anomalies` | Scan for unusual access patterns | -| `verify_audit_chain` | Verify tamper-evident hash chain integrity | -| `export_audit` | Export audit events in jsonl, json, or csv format | -| `health_check` | Full health report | -| `status_dashboard` | Launch the quantum status dashboard (SSE) — live KPIs, health, secrets table, manifest, policy, approvals, hooks, agent memory, anomalies, and audit feed | -| `agent_scan` | Run autonomous agent scan | +| `audit_log` | Query the tamper-evident audit log filtered by key, action, and limit | +| `detect_anomalies` | Surface burst-read and off-hours findings from audit history | +| `verify_audit_chain` | Recompute the audit hash chain and report the first break point if tampered | +| `export_audit` | Export audit events as jsonl, json, or csv for archival/SIEM | +| `health_check` | Read-only scope sweep: decay/stale/expired counts plus current anomalies | +| `status_dashboard` | Start a local SSE dashboard with live KPIs, secrets, hooks, and audit feed | +| `agent_scan` | Multi-project health pass with optional `autoRotate` for expired secrets | ### Governance & Policy Tools | Tool | Description | |------|-------------| -| `check_policy` | Check if an action (tool use, key read, exec) is allowed by project policy | -| `get_policy_summary` | Get a summary of the project's governance policy configuration | -| `rotate_secret` | Attempt issuer-native rotation via detected or specified provider | -| `ci_validate_secrets` | CI-oriented batch validation of all secrets with structured pass/fail report | +| `check_policy` | Dry-run a tool/key/exec action against `.q-ring.json` policy without performing it | +| `get_policy_summary` | High-level overview of policy rule counts and approval/rotation requirements | +| `rotate_secret` | Ask the upstream provider to issue a new credential and store it back in the keyring | +| `ci_validate_secrets` | Batch-validate every accessible secret in scope and return a structured pass/fail report | ### Cursor / Kiro Configuration diff --git a/cursor-plugin/.cursor-plugin/plugin.json b/cursor-plugin/.cursor-plugin/plugin.json index f86e029..b6ed82e 100644 --- a/cursor-plugin/.cursor-plugin/plugin.json +++ b/cursor-plugin/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "qring", "description": "Quantum keyring for AI agents — manage secrets, scan for leaks, rotate keys, and enforce policy directly from Cursor.", - "version": "0.11.5", + "version": "0.11.7", "author": { "name": "I4cTime" }, diff --git a/package.json b/package.json index 141307d..82d462b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@i4ctime/q-ring", - "version": "0.11.5", + "version": "0.11.7", "mcpName": "io.github.I4cTime/q-ring", "description": "Quantum keyring for AI coding tools — Cursor, Kiro, Claude Code. Secrets, superposition, entanglement, MCP.", "type": "module", diff --git a/server.json b/server.json index cbf8314..f06224f 100644 --- a/server.json +++ b/server.json @@ -6,12 +6,12 @@ "url": "https://github.com/I4cTime/quantum_ring", "source": "github" }, - "version": "0.11.5", + "version": "0.11.7", "packages": [ { "registryType": "npm", "identifier": "@i4ctime/q-ring", - "version": "0.11.5", + "version": "0.11.7", "transport": { "type": "stdio" } diff --git a/src/mcp/tools/_shared.ts b/src/mcp/tools/_shared.ts index 04dd3b4..af3ecb1 100644 --- a/src/mcp/tools/_shared.ts +++ b/src/mcp/tools/_shared.ts @@ -52,21 +52,31 @@ export const commonSchemas = { teamId: z .string() .optional() - .describe("Team identifier for team-scoped secrets"), + .describe( + "Team identifier for team-scoped secrets. Required only when scope='team'. Example: 'acme-platform'.", + ), orgId: z .string() .optional() - .describe("Org identifier for org-scoped secrets"), + .describe( + "Organization identifier for org-scoped secrets. Required only when scope='org'. Example: 'acme-corp'.", + ), scope: z .enum(["global", "project", "team", "org"]) .optional() - .describe("Scope: global, project, team, or org"), + .describe( + "Where the secret lives. 'global' = user keyring (default if omitted on reads), 'project' = scoped to projectPath, 'team' = team-shared (needs teamId), 'org' = org-shared (needs orgId).", + ), projectPath: z .string() .optional() - .describe("Project root path for project-scoped secrets"), + .describe( + "Absolute path to the project root for project-scoped secrets and policy resolution. Defaults to the MCP server's current working directory when omitted.", + ), env: z .string() .optional() - .describe("Environment for superposition collapse (e.g., dev, staging, prod)"), + .describe( + "Environment slug used to collapse superposition when a secret has multiple per-env states. Examples: 'dev', 'staging', 'prod'. If omitted, the secret's defaultEnv is used.", + ), } as const; diff --git a/src/mcp/tools/agent.ts b/src/mcp/tools/agent.ts index 78ee4fb..c33fa0a 100644 --- a/src/mcp/tools/agent.ts +++ b/src/mcp/tools/agent.ts @@ -6,10 +6,22 @@ import { text, enforceToolPolicy } from "./_shared.js"; export function registerAgentTools(server: McpServer): void { server.tool( "agent_remember", - "[agent] Store a key-value pair in encrypted agent memory that persists across sessions. Use this to remember decisions, rotation history, or project-specific context.", + [ + "[agent] Persist a non-secret key/value note in encrypted, on-disk agent memory that survives across MCP sessions.", + "Use to record stable agent context — last rotation date for a key, the user's deployment preferences, decisions taken in earlier sessions; do NOT use this to store secrets (use `set_secret` instead) and prefer chat scratchpad for purely transient state.", + "Mutates the encrypted memory store. Idempotent: rewriting the same key with a new value simply overwrites. Returns 'Remembered \"KEY\"' on success.", + ].join(" "), { - key: z.string().describe("Memory key"), - value: z.string().describe("Value to store"), + key: z + .string() + .describe( + "Memory key (free-form string). Convention: lowercase dotted namespaces, e.g. 'project.lastDeploy'.", + ), + value: z + .string() + .describe( + "Plain-string value to store. JSON-stringify structured data on the caller side if needed.", + ), }, async (params) => { const toolBlock = enforceToolPolicy("agent_remember"); @@ -22,9 +34,18 @@ export function registerAgentTools(server: McpServer): void { server.tool( "agent_recall", - "[agent] Retrieve a value from agent memory, or list all stored keys if no key is provided.", + [ + "[agent] Read a value from encrypted agent memory, or list every stored key when no specific key is supplied.", + "Use at the start of an agent loop to rehydrate prior context, or to look up a single remembered fact; prefer `get_project_context` for a redacted overview of secrets and `get_secret` for actual credential values.", + "Read-only. With a `key` argument: returns JSON `{ ok, data: { key, value } }` or a not-found error. Without `key`: returns a JSON listing of every stored key (no values), or 'Agent memory is empty'.", + ].join(" "), { - key: z.string().optional().describe("Memory key to recall (omit to list all)"), + key: z + .string() + .optional() + .describe( + "Memory key to read. Omit to list every stored key (without values).", + ), }, async (params) => { const toolBlock = enforceToolPolicy("agent_recall"); @@ -46,9 +67,13 @@ export function registerAgentTools(server: McpServer): void { server.tool( "agent_forget", - "[agent] Delete a key from agent memory.", + [ + "[agent] Permanently delete a single key from encrypted agent memory.", + "Use to retract obsolete or misremembered context; prefer overwriting via `agent_remember` when you just want to update the value, and use `delete_secret` for actual credentials (which never live in agent memory).", + "Destructive: there is no recycle bin. Returns 'Forgot \"KEY\"' on success or a not-found error if the key was already absent.", + ].join(" "), { - key: z.string().describe("Memory key to forget"), + key: z.string().describe("Memory key to delete."), }, async (params) => { const toolBlock = enforceToolPolicy("agent_forget"); diff --git a/src/mcp/tools/audit.ts b/src/mcp/tools/audit.ts index ebd06d8..08ef336 100644 --- a/src/mcp/tools/audit.ts +++ b/src/mcp/tools/audit.ts @@ -14,9 +14,18 @@ const { teamId, orgId, scope, projectPath } = commonSchemas; export function registerAuditTools(server: McpServer): void { server.tool( "audit_log", - "[audit] Query the audit log for secret access history (observer effect). Shows who accessed what and when.", + [ + "[audit] Query the q-ring audit log — a tamper-evident record of every read/write/delete touching a secret.", + "Use to investigate 'who accessed KEY recently?' or to feed an agent the access timeline for a specific credential; prefer `detect_anomalies` for automated unusual-pattern detection and `health_check` for decay-state-plus-anomalies in one call.", + "Read-only. Returns one line per event in chronological order, formatted `timestamp | action | key | [scope] | env:NAME | detail`. Returns 'No audit events found' when the filter matches nothing.", + ].join(" "), { - key: z.string().optional().describe("Filter by key"), + key: z + .string() + .optional() + .describe( + "Limit to events touching this exact key. Omit for the full log.", + ), action: z .enum([ "read", @@ -31,8 +40,16 @@ export function registerAuditTools(server: McpServer): void { "collapse", ]) .optional() - .describe("Filter by action"), - limit: z.number().optional().default(20).describe("Max events to return"), + .describe( + "Limit to a single action verb (e.g. 'read' to see only reads). Omit for all actions.", + ), + limit: z + .number() + .optional() + .default(20) + .describe( + "Maximum events to return, newest first. Defaults to 20. Increase for deeper investigations.", + ), }, async (params) => { const toolBlock = enforceToolPolicy("audit_log"); @@ -61,9 +78,18 @@ export function registerAuditTools(server: McpServer): void { server.tool( "detect_anomalies", - "[audit] Read-only scan of audit history for burst reads and unusual-hour access (text lines per finding). Optional key filter. Use health_check for full decay inventory + anomaly count in scope; use agent_scan for multi-project JSON reports or optional auto-rotation. Does not mutate secrets.", + [ + "[audit] Scan the audit history for suspicious access patterns — burst reads of the same key, off-hours access, and other heuristics.", + "Use as a quick triage signal when investigating a single key or before letting an agent rotate credentials; prefer `health_check` for a scope-wide decay+anomaly summary, and `agent_scan` for multi-project JSON reports with optional auto-rotation.", + "Read-only; never mutates secrets or the audit log. Returns one line per finding formatted `[type] description`, or 'No anomalies detected' when the log looks clean.", + ].join(" "), { - key: z.string().optional().describe("Check anomalies for a specific key"), + key: z + .string() + .optional() + .describe( + "If provided, narrow the scan to this exact key. Omit to scan across every key in the audit log.", + ), }, async (params) => { const toolBlock = enforceToolPolicy("detect_anomalies"); @@ -79,7 +105,11 @@ export function registerAuditTools(server: McpServer): void { server.tool( "health_check", - "[health] Read-only scoped pass: decay/stale/expired counts, per-secret issue lines, plus audit-derived anomalies. No writes. Use check_project for .q-ring.json manifest compliance; use detect_anomalies for audit-pattern-only triage; use agent_scan for multi-project JSON or optional autoRotate credential replacement.", + [ + "[health] Run a single read-only sweep over every secret in the requested scope and report counts of healthy/stale/expired secrets plus any current audit anomalies.", + "Use as the default 'is everything OK?' command for an agent or operator; prefer `check_project` to validate manifest compliance specifically, `detect_anomalies` for audit-only triage, and `agent_scan` for multi-project JSON output or optional auto-rotation.", + "Read-only — never writes. Returns a multi-line text summary: header counts (Total / Healthy / Stale / Expired / No decay / Anomalies), then per-secret `EXPIRED:` / `STALE:` issue lines, then per-anomaly `[type] description` lines.", + ].join(" "), { scope, projectPath, @@ -140,7 +170,11 @@ export function registerAuditTools(server: McpServer): void { server.tool( "verify_audit_chain", - "[audit] Verify the tamper-evident hash chain of the audit log. Returns integrity status and the first break point if tampered.", + [ + "[audit] Recompute the SHA-256 hash chain over the audit log and confirm no event has been mutated, deleted, or reordered.", + "Use periodically as a tamper-evidence check, or whenever you suspect the audit log has been touched outside q-ring; the result is informational — this tool does not repair the chain if it is broken.", + "Read-only. Returns JSON `{ ok, valid, brokenAt? }` where `valid` is `true` for an intact chain and `brokenAt` (when present) names the first event whose hash did not match.", + ].join(" "), {}, async () => { const toolBlock = enforceToolPolicy("verify_audit_chain"); @@ -153,15 +187,31 @@ export function registerAuditTools(server: McpServer): void { server.tool( "export_audit", - "[audit] Export audit events in a portable format (jsonl, json, or csv) with optional time range filtering.", + [ + "[audit] Export the audit log as a portable text artifact suitable for archiving or feeding into another SIEM/analyzer.", + "Use for compliance exports, after-the-fact investigations, or to hand the trail to a non-MCP consumer; prefer `audit_log` for an in-conversation tail and `verify_audit_chain` to confirm integrity before exporting.", + "Read-only. Returns the rendered text directly (no JSON wrapper). 'jsonl' is one event per line; 'json' is a single array; 'csv' is a header row plus events. Time filters are applied to the event timestamps before formatting.", + ].join(" "), { - since: z.string().optional().describe("Start date (ISO 8601)"), - until: z.string().optional().describe("End date (ISO 8601)"), + since: z + .string() + .optional() + .describe( + "Inclusive lower bound on event timestamp, ISO 8601. Example: '2026-04-01T00:00:00Z'. Omit for no lower bound.", + ), + until: z + .string() + .optional() + .describe( + "Inclusive upper bound on event timestamp, ISO 8601. Omit for now/no upper bound.", + ), format: z .enum(["jsonl", "json", "csv"]) .optional() .default("jsonl") - .describe("Output format"), + .describe( + "Output format. 'jsonl' (default) is most stream-friendly; 'json' is a single array; 'csv' is spreadsheet-friendly.", + ), }, async (params) => { const toolBlock = enforceToolPolicy("export_audit"); diff --git a/src/mcp/tools/hooks.ts b/src/mcp/tools/hooks.ts index dd331f1..574e0c4 100644 --- a/src/mcp/tools/hooks.ts +++ b/src/mcp/tools/hooks.ts @@ -12,39 +12,79 @@ import { text, enforceToolPolicy } from "./_shared.js"; export function registerHookTools(server: McpServer): void { server.tool( "register_hook", - "[hooks] Register a webhook/callback that fires when a secret is updated, deleted, or rotated. Supports shell commands, HTTP webhooks, and process signals.", + [ + "[hooks] Register a side-effect (shell command, HTTP webhook, or process signal) that fires automatically when a matching secret is written, deleted, or rotated.", + "Use to keep external systems in sync (restart a service after rotation, post to Slack on delete, kick a build); prefer `agent_remember` for storing facts an agent should recall later, and `register_hook` is not the right tool for time-based scheduled rotation (use `agent_scan` for that).", + "Mutates the hook registry on disk. At least one match criterion (`key`, `keyPattern`, or `tag`) is required — calls without any return an error. Returns JSON of the registered hook entry including its assigned `id` (use that `id` with `remove_hook`).", + ].join(" "), { - type: z.enum(["shell", "http", "signal"]).describe("Hook type"), - key: z.string().optional().describe("Trigger on exact key match"), + type: z + .enum(["shell", "http", "signal"]) + .describe( + "Hook delivery mechanism. 'shell' runs a local command, 'http' POSTs JSON to a URL, 'signal' sends an OS signal to a named process.", + ), + key: z + .string() + .optional() + .describe( + "Trigger only on this exact key name. Pick at most one of `key` / `keyPattern` / `tag` (or combine for stricter matching).", + ), keyPattern: z .string() .optional() - .describe("Trigger on key glob pattern (e.g. DB_*)"), - tag: z.string().optional().describe("Trigger on secrets with this tag"), + .describe( + "Trigger on any key matching this glob pattern. Examples: 'DB_*', 'STRIPE_*'.", + ), + tag: z + .string() + .optional() + .describe( + "Trigger on any secret carrying this exact tag. Combinable with key/keyPattern as an AND filter.", + ), scope: z .enum(["global", "project"]) .optional() - .describe("Trigger only for this scope"), + .describe( + "Restrict the hook to secrets in this scope. Omit to fire across both global and project secrets.", + ), actions: z .array(z.enum(["write", "delete", "rotate"])) .optional() .default(["write", "delete", "rotate"]) - .describe("Which actions trigger this hook"), + .describe( + "Which lifecycle actions trigger this hook. Defaults to all three.", + ), command: z .string() .optional() - .describe("Shell command to execute (for shell type)"), - url: z.string().optional().describe("URL to POST to (for http type)"), + .describe( + "Required when type='shell'. The literal shell command to run; q-ring exposes the matching key as $QRING_HOOK_KEY and action as $QRING_HOOK_ACTION.", + ), + url: z + .string() + .optional() + .describe( + "Required when type='http'. Full URL to POST a JSON body `{ id, key, scope, action, timestamp }` to (the value itself is never sent).", + ), signalTarget: z .string() .optional() - .describe("Process name or PID (for signal type)"), + .describe( + "Required when type='signal'. Either a numeric PID or a process name resolvable via `ps`.", + ), signalName: z .string() .optional() .default("SIGHUP") - .describe("Signal to send (for signal type)"), - description: z.string().optional().describe("Human-readable description"), + .describe( + "Signal name to send (e.g. 'SIGHUP', 'SIGUSR1'). Defaults to SIGHUP, which most daemons treat as 'reload config'.", + ), + description: z + .string() + .optional() + .describe( + "Free-text human-readable description, surfaced by `list_hooks` and the dashboard.", + ), }, async (params) => { const toolBlock = enforceToolPolicy("register_hook"); @@ -81,7 +121,11 @@ export function registerHookTools(server: McpServer): void { server.tool( "list_hooks", - "[hooks] List all registered secret change hooks with their match criteria, type, and status.", + [ + "[hooks] Enumerate every registered lifecycle hook with its match criteria, delivery type, enabled flag, and description.", + "Use to find a hook's `id` before calling `remove_hook`, audit what side effects are wired up, or diagnose why a hook did not fire.", + "Read-only. Returns pretty-printed JSON array of hook entries, or 'No hooks registered' when the registry is empty.", + ].join(" "), {}, async () => { const toolBlock = enforceToolPolicy("list_hooks"); @@ -95,9 +139,17 @@ export function registerHookTools(server: McpServer): void { server.tool( "remove_hook", - "[hooks] Remove a lifecycle hook entry by id from the hook registry only (stops callbacks; does not touch secret values). Call list_hooks first for ids. Contrast delete_secret (credential removal) or tunnel_destroy (ephemeral tunnel). Returns success or not-found; subject to tool policy.", + [ + "[hooks] Detach a single lifecycle hook by its registry id so it stops firing.", + "Use to retire a specific webhook/command without touching any secrets; prefer `delete_secret` to remove a credential and `tunnel_destroy` for ephemeral tunnels.", + "Mutates the hook registry only — does not touch secret values, audit log, or env states. Idempotent in spirit: removing an already-absent id returns a not-found error rather than partial work. Returns 'Removed hook ID' on success.", + ].join(" "), { - id: z.string().describe("Hook ID to remove"), + id: z + .string() + .describe( + "Hook id returned by `register_hook` or visible in `list_hooks` (opaque string).", + ), }, async (params) => { const toolBlock = enforceToolPolicy("remove_hook"); diff --git a/src/mcp/tools/policy.ts b/src/mcp/tools/policy.ts index 797d8be..3da073b 100644 --- a/src/mcp/tools/policy.ts +++ b/src/mcp/tools/policy.ts @@ -13,20 +13,35 @@ const { projectPath } = commonSchemas; export function registerPolicyTools(server: McpServer): void { server.tool( "check_policy", - "[policy] Check if an action is allowed by the project's governance policy. Returns the policy decision and source.", + [ + "[policy] Ask whether a single intended action would be allowed by the project's `.q-ring.json` policy without actually performing it.", + "Use as a dry-run before calling a potentially-blocked tool, attempting to read a sensitive key, or invoking `exec_with_secrets` with a non-trivial command; prefer `get_policy_summary` for a one-shot overview of the entire policy.", + "Read-only. Returns JSON `{ allowed, reason?, policySource }` describing the decision. Returns an error 'Missing required parameter for the selected action type' if the matching argument for the chosen `action` is not supplied.", + ].join(" "), { action: z .enum(["tool", "key_read", "exec"]) - .describe("Type of policy check"), + .describe( + "Which policy surface to query. 'tool' = MCP tool gate (needs `toolName`); 'key_read' = secret read gate (needs `key`); 'exec' = exec_with_secrets command gate (needs `command`).", + ), toolName: z .string() .optional() - .describe("Tool name to check (for action=tool)"), - key: z.string().optional().describe("Secret key to check (for action=key_read)"), + .describe( + "Tool id to evaluate, e.g. 'rotate_secret'. Required when `action` is 'tool'.", + ), + key: z + .string() + .optional() + .describe( + "Secret key name to evaluate. Required when `action` is 'key_read'.", + ), command: z .string() .optional() - .describe("Command to check (for action=exec)"), + .describe( + "Command to evaluate against the exec allowlist/denylist. Required when `action` is 'exec'.", + ), projectPath, }, async (params) => { @@ -51,7 +66,11 @@ export function registerPolicyTools(server: McpServer): void { server.tool( "get_policy_summary", - "[policy] Get a summary of the project's governance policy configuration.", + [ + "[policy] Return a high-level summary of the project's `.q-ring.json` governance policy — counts of allow/deny rules for tools, key reads, exec commands, plus approval and rotation requirements.", + "Use to orient an agent (or the user) on what guardrails are active before attempting policy-restricted actions; prefer `check_policy` for a precise per-action verdict.", + "Read-only. Returns pretty-printed JSON; missing policy file returns an empty/default summary rather than an error so callers can branch on the counts.", + ].join(" "), { projectPath, }, diff --git a/src/mcp/tools/project.ts b/src/mcp/tools/project.ts index a63ea63..b4ac0b6 100644 --- a/src/mcp/tools/project.ts +++ b/src/mcp/tools/project.ts @@ -10,7 +10,11 @@ const { teamId, orgId, scope, projectPath, env } = commonSchemas; export function registerProjectTools(server: McpServer): void { server.tool( "check_project", - "[project] Validate project secrets against the .q-ring.json manifest. Returns which required secrets are present, missing, expired, or stale. Use this to verify project readiness.", + [ + "[project] Compare the keys declared in the project's `.q-ring.json` manifest against what is actually present in the keyring.", + "Use as the canonical 'is this project ready to run' gate before starting a dev server, deploying, or onboarding a teammate; prefer `health_check` for a scope-wide decay sweep (no manifest), and `agent_scan` for multi-project scans with optional auto-rotation.", + "Read-only; does not mutate the keyring or audit log materially beyond a 'list' read. Returns JSON `{ total, present, missing, expired, stale, ready, secrets: [...] }` where `ready` is true only when nothing is missing or expired. Errors with 'No secrets manifest found in .q-ring.json' if the project has no manifest.", + ].join(" "), { projectPath, }, @@ -87,7 +91,11 @@ export function registerProjectTools(server: McpServer): void { server.tool( "env_generate", - "[project] Generate .env file content from the project manifest (.q-ring.json). Resolves each declared secret from q-ring, collapses superposition, and returns .env formatted output. Warns about missing or expired secrets.", + [ + "[project] Render a complete `.env` file body from the project's `.q-ring.json` manifest, resolving each declared key from the keyring.", + "Use when a build step or local runtime needs a real `.env` materialized on disk and you want exactly the keys the manifest declares; prefer `export_secrets` when you want every key in scope (manifest-agnostic) and `exec_with_secrets` to inject secrets into a child process without writing them to a file.", + "Reads values (records 'read' audit events) and collapses superposition for the requested env. Returns the raw `.env` text, with `# MISSING (required): KEY` / `# EXPIRED: KEY` / `# STALE: KEY` warnings appended as comments. Missing keys appear as commented-out `# KEY=` placeholders so the file remains a valid drop-in.", + ].join(" "), { projectPath, env, @@ -147,7 +155,11 @@ export function registerProjectTools(server: McpServer): void { server.tool( "detect_environment", - "[project] Detect the current environment context (wavefunction collapse). Returns the detected environment and its source (NODE_ENV, git branch, project config, etc.).", + [ + "[project] Resolve which environment slug (e.g. 'dev', 'staging', 'prod') the current invocation should collapse to.", + "Use before reading secrets when you want to mirror the same env q-ring would auto-pick (e.g. to log it, or to pass through to another tool); prefer passing an explicit `env` to `get_secret`/`env_generate` when you already know which env you want.", + "Read-only; checks the QRING_ENV env var, NODE_ENV, the project's `.q-ring.json`, and the current git branch in priority order. Returns JSON `{ env, source }` (e.g. `{ env: 'dev', source: 'NODE_ENV' }`), or a plain message indicating that no env could be detected.", + ].join(" "), { projectPath, }, @@ -174,7 +186,11 @@ export function registerProjectTools(server: McpServer): void { server.tool( "get_project_context", - "[agent] Get a safe, redacted overview of the project's secrets, environment, manifest, providers, hooks, and recent audit activity. No secret values are ever exposed. Use this to understand what secrets exist before asking to read them.", + [ + "[agent] Return a single redacted snapshot of everything an AI agent typically wants to know about this project: secrets present (keys + metadata only), detected env, manifest declarations, configured providers, registered hooks, and recent audit activity.", + "Use this as the very first call in a session to orient the agent before it asks for any individual secret; prefer `list_secrets` for a flat key listing, `check_project` for manifest-vs-keyring drift, and `audit_log` for a deeper access trail.", + "Read-only and value-safe — no plaintext secret values are ever included. Returns a single pretty-printed JSON document; shape is intentionally broad and may grow over time, so read defensively.", + ].join(" "), { scope, projectPath, diff --git a/src/mcp/tools/secrets.ts b/src/mcp/tools/secrets.ts index 7ce2e13..77ca821 100644 --- a/src/mcp/tools/secrets.ts +++ b/src/mcp/tools/secrets.ts @@ -28,9 +28,17 @@ const { teamId, orgId, scope, projectPath, env } = commonSchemas; export function registerSecretTools(server: McpServer): void { server.tool( "get_secret", - "[secrets] Retrieve a secret by key. Collapses superposition if the secret has multiple environment states. Records access in audit log (observer effect).", + [ + "[secrets] Read the plaintext value of a single secret from the q-ring keyring.", + "Use when an agent needs the actual credential to call an external API or inject into a runtime; prefer `inspect_secret` to see metadata only, `has_secret` for presence-only checks, and `exec_with_secrets` to run a command without exposing the value to chat.", + "Side effects: collapses superposition (selects the per-env state) and writes a 'read' event to the audit log (observer effect). Subject to project tool/key policy and may be denied with a 'Policy Denied' message. Returns JSON `{ ok, data: { key, value } }` on success or an error message if missing/blocked.", + ].join(" "), { - key: z.string().describe("The secret key name"), + key: z + .string() + .describe( + "Exact secret key name as stored in the keyring (case-sensitive). Example: 'OPENAI_API_KEY'.", + ), scope, projectPath, env, @@ -64,20 +72,38 @@ export function registerSecretTools(server: McpServer): void { server.tool( "list_secrets", - "[secrets] List all secret keys with quantum metadata (scope, decay status, superposition states, entanglement, access count). Values are never exposed. Supports filtering by tag, expiry state, and key pattern.", + [ + "[secrets] List secret keys and quantum metadata in the requested scope, never the values.", + "Use to discover what secrets exist before reading or writing; pair with `inspect_secret` for full metadata on one key, `analyze_secrets` for usage trends, or `health_check` for decay/anomaly summaries.", + "Read-only; safe to call repeatedly. Returns JSON `{ ok, data: { entries: [...] } }` where each entry has scope, key, stateKeys (env names if superposed), expired, stale, lifetimePercent, timeRemaining, entangledCount, accessCount.", + ].join(" "), { scope, projectPath, - tag: z.string().optional().describe("Filter by tag"), - expired: z.boolean().optional().describe("Show only expired secrets"), + tag: z + .string() + .optional() + .describe( + "Return only secrets that include this exact tag (case-sensitive). Example: 'production'.", + ), + expired: z + .boolean() + .optional() + .describe( + "If true, return only secrets whose decay TTL has elapsed (lifetimePercent >= 100).", + ), stale: z .boolean() .optional() - .describe("Show only stale secrets (75%+ decay)"), + .describe( + "If true, return only secrets in the stale window (lifetimePercent >= 75 and not yet expired).", + ), filter: z .string() .optional() - .describe("Glob pattern on key name (e.g., 'API_*')"), + .describe( + "Glob pattern matched against the key name. Supports `*` and `?`. Examples: 'API_*', 'STRIPE_?_KEY'.", + ), teamId, orgId, }, @@ -123,24 +149,48 @@ export function registerSecretTools(server: McpServer): void { server.tool( "set_secret", - "[secrets] Create or overwrite a secret value plus optional metadata (TTL/decay, per-env superposition, description, tags). Overwrites existing values for the same key/scope; records access in the audit log. Use import_dotenv for bulk .env ingest. Subject to tool policy; no external rate limits beyond provider calls when validating elsewhere.", + [ + "[secrets] Create or overwrite a single secret value, optionally with TTL/decay, per-env superposition, description, tags, and rotation hints.", + "Use to add or update one key at a time; prefer `import_dotenv` for bulk .env ingest, `generate_secret` (with saveAs) to generate-and-store in one step, and `entangle_secrets` instead of duplicating the same value under two keys.", + "Mutates the keyring (overwrites any existing value at the same key/scope), writes a 'write' event to the audit log, and triggers any matching hooks. Subject to tool policy. Returns a short confirmation text like '[scope] KEY saved' (or '[scope] KEY set for env:NAME' when `env` is provided).", + ].join(" "), { - key: z.string().describe("The secret key name"), - value: z.string().describe("The secret value"), + key: z + .string() + .describe( + "Secret key name (UPPER_SNAKE_CASE recommended). Example: 'STRIPE_SECRET_KEY'.", + ), + value: z + .string() + .describe( + "The secret value to store. Stored as-is; never logged or echoed. May be empty only when `env` is provided to register a new env without a default.", + ), scope: scope.default("global"), projectPath, env: z .string() .optional() .describe( - "If provided, sets the value for this specific environment (superposition)", + "If set, writes this value to the named per-env state (superposition) instead of the default slot. Existing default value is preserved as state 'default'. Example: 'prod'.", ), ttlSeconds: z .number() .optional() - .describe("Time-to-live in seconds (quantum decay)"), - description: z.string().optional().describe("Human-readable description"), - tags: z.array(z.string()).optional().describe("Tags for organization"), + .describe( + "Quantum decay window in seconds. After this many seconds the secret is marked expired (still readable, but `has_secret` returns false and `health_check` flags it). Omit for no decay.", + ), + description: z + .string() + .optional() + .describe( + "Free-text human-readable description shown in `inspect_secret` and the dashboard.", + ), + tags: z + .array(z.string()) + .optional() + .describe( + "Tag list for filtering and hook matching. Example: ['production', 'payments'].", + ), rotationFormat: z .enum([ "hex", @@ -152,11 +202,15 @@ export function registerSecretTools(server: McpServer): void { "password", ]) .optional() - .describe("Format for auto-rotation when this secret expires"), + .describe( + "Format used by `agent_scan --autoRotate` and `rotate_secret` when this secret expires. Pick the format that matches the upstream service's accepted shape.", + ), rotationPrefix: z .string() .optional() - .describe("Prefix for auto-rotation (e.g. 'sk-')"), + .describe( + "Literal prefix prepended on auto-rotation (only used with rotationFormat 'api-key' or 'token'). Example: 'sk-'.", + ), teamId, orgId, }, @@ -206,9 +260,15 @@ export function registerSecretTools(server: McpServer): void { server.tool( "delete_secret", - "[secrets] Permanently remove a secret value from the keyring for the given scope/path (not recoverable from q-ring). Does not remove hooks, tunnels, or entanglement metadata alone—use remove_hook, tunnel_destroy, or disentangle_secrets respectively. Returns success or not-found text; subject to tool policy.", + [ + "[secrets] Permanently remove a secret value (and all its env states) from the keyring for the given scope.", + "Use when a credential is being retired or was created in error; prefer `disentangle_secrets` to break a sync link without erasing values, `remove_hook` to detach lifecycle callbacks, and `tunnel_destroy` for ephemeral tunnels.", + "Destructive and not undoable from q-ring (no built-in trash). Writes a 'delete' event to the audit log and fires matching hooks. Returns 'Deleted \"KEY\"' on success or a not-found error if the key did not exist in the requested scope. Subject to tool policy.", + ].join(" "), { - key: z.string().describe("The secret key name"), + key: z + .string() + .describe("Exact secret key name to delete. Example: 'OLD_API_KEY'."), scope, projectPath, teamId, @@ -228,9 +288,15 @@ export function registerSecretTools(server: McpServer): void { server.tool( "has_secret", - "[secrets] Check if a secret exists. Returns boolean. Never reveals the value. Respects decay — expired secrets return false.", + [ + "[secrets] Check whether a secret exists in the requested scope without reading the value.", + "Use as a cheap precondition before reading or writing — for example, to skip prompting the user for a key that is already configured. Prefer `inspect_secret` when you also need metadata.", + "Read-only; does not record a 'read' in the audit log. Decay-aware: returns 'false' for expired secrets even though the value is still in the store. Returns the literal text 'true' or 'false'.", + ].join(" "), { - key: z.string().describe("The secret key name"), + key: z + .string() + .describe("Exact secret key name. Example: 'GITHUB_TOKEN'."), scope, projectPath, teamId, @@ -246,21 +312,31 @@ export function registerSecretTools(server: McpServer): void { server.tool( "export_secrets", - "[secrets] Export secrets as .env or JSON format. Collapses superposition. Supports filtering by specific keys or tags.", + [ + "[secrets] Render multiple secrets as a single .env or JSON document for piping into another tool or file.", + "Use to materialize secrets for a one-off export or copy; prefer `env_generate` when you want output driven by the project's `.q-ring.json` manifest, and `teleport_pack` for an encrypted bundle to share between machines.", + "Reads values (collapses superposition for the requested env) and writes one 'export' event per included secret to the audit log. Returns the rendered text directly (no JSON wrapper). Returns an error if no secrets matched the filters. Values are surfaced in plaintext — handle with care.", + ].join(" "), { format: z .enum(["env", "json"]) .optional() .default("env") - .describe("Output format"), + .describe( + "'env' renders KEY=\"value\" lines suitable for a .env file; 'json' renders an object keyed by secret name. Defaults to 'env'.", + ), keys: z .array(z.string()) .optional() - .describe("Only export these specific key names"), + .describe( + "Whitelist of exact key names to include. If omitted, every key in scope is considered (subject to `tags`).", + ), tags: z .array(z.string()) .optional() - .describe("Only export secrets with any of these tags"), + .describe( + "Include only secrets tagged with at least one of these tags. Combined with `keys` as an AND filter when both are supplied.", + ), scope, projectPath, env, @@ -285,21 +361,33 @@ export function registerSecretTools(server: McpServer): void { server.tool( "import_dotenv", - "[secrets] Import secrets from .env file content. Parses standard dotenv syntax (comments, quotes, multiline escapes) and stores each key/value pair in q-ring.", + [ + "[secrets] Parse standard dotenv-formatted text and store each key/value pair into the keyring in one batch.", + "Use when migrating an existing `.env` file into q-ring or onboarding a new project; prefer `set_secret` for a single key, and `teleport_unpack` to import an encrypted bundle.", + "Mutates the keyring (one write per parsed key) and emits a 'write' audit event for each. Supports comments, single/double quotes, and `\\n` escapes. Returns a multiline summary listing imported keys and any skipped (existing) keys; in dryRun mode no writes happen and the same summary is produced for review.", + ].join(" "), { - content: z.string().describe("The .env file content to parse and import"), + content: z + .string() + .describe( + "Raw .env file content as a single string (newline-separated KEY=VALUE lines, comments allowed).", + ), scope: scope.default("global"), projectPath, skipExisting: z .boolean() .optional() .default(false) - .describe("Skip keys that already exist in q-ring"), + .describe( + "If true, leave already-present keys untouched and add them to the 'skipped' list instead of overwriting.", + ), dryRun: z .boolean() .optional() .default(false) - .describe("Preview what would be imported without saving"), + .describe( + "If true, parse and report what would happen but do not write to the keyring. Useful for previewing imports before committing.", + ), }, async (params) => { const toolBlock = enforceToolPolicy("import_dotenv", params.projectPath); @@ -332,9 +420,15 @@ export function registerSecretTools(server: McpServer): void { server.tool( "inspect_secret", - "[secrets] Show full quantum state of a secret: superposition states, decay status, entanglement links, access history. Never reveals the actual value.", + [ + "[secrets] Show full metadata for a single secret — env states, decay window, entanglement links, access counters — without ever revealing the value.", + "Use when you need to understand the shape of a key before reading it or to debug 'why is this expired/stale'; prefer `get_secret` for the actual value, `list_secrets` for a many-key overview, and `audit_log` for the full access timeline.", + "Read-only; does not write a 'read' event since the value is not exposed. Returns pretty-printed JSON with fields: key, scope, type ('superposition'|'collapsed'), created, updated, accessCount, lastAccessed, environments, defaultEnv, decay { expired, stale, lifetimePercent, timeRemaining }, entangled, description, tags. Errors with not-found if the key is absent.", + ].join(" "), { - key: z.string().describe("The secret key name"), + key: z + .string() + .describe("Exact secret key name to inspect. Example: 'OPENAI_API_KEY'."), scope, projectPath, teamId, @@ -388,7 +482,11 @@ export function registerSecretTools(server: McpServer): void { server.tool( "generate_secret", - "[secrets] Generate a cryptographic secret (quantum noise). Formats: hex, base64, alphanumeric, uuid, api-key, token, password. Optionally save directly to the keyring.", + [ + "[secrets] Generate a cryptographically random secret using Node's CSPRNG and optionally store it in the keyring in one step.", + "Use to create new credentials that you control (signing keys, internal tokens, passwords); for issuer-issued credentials (Stripe/OpenAI etc.) use `rotate_secret` to ask the upstream provider for a fresh key, and use `set_secret` for values you already have in hand.", + "If `saveAs` is provided this mutates the keyring (one 'write' event) and returns a summary like 'Generated and saved as \"KEY\" (FORMAT, ~N bits entropy)'. Without `saveAs` the call is read-only and returns JSON `{ ok, data: { value } }` containing the freshly generated string.", + ].join(" "), { format: z .enum([ @@ -402,13 +500,27 @@ export function registerSecretTools(server: McpServer): void { ]) .optional() .default("api-key") - .describe("Output format"), - length: z.number().optional().describe("Length in bytes or characters"), - prefix: z.string().optional().describe("Prefix for api-key/token format"), + .describe( + "Output shape. 'hex' / 'base64' / 'alphanumeric' = raw random string of `length` characters; 'uuid' = RFC4122 v4; 'api-key' / 'token' = random alphanumeric with optional `prefix`; 'password' = mixed-case alphanumeric with symbols. Defaults to 'api-key'.", + ), + length: z + .number() + .optional() + .describe( + "Number of characters (or bytes for hex/base64) to generate. Ignored for 'uuid'. Defaults to a sensible per-format value (e.g. 32 for api-key).", + ), + prefix: z + .string() + .optional() + .describe( + "Literal prefix prepended to the random portion. Only meaningful for 'api-key' and 'token'. Example: 'sk-' or 'svc_'.", + ), saveAs: z .string() .optional() - .describe("If provided, save the generated secret with this key name"), + .describe( + "If provided, store the generated value at this key name in the keyring (one mutation). Omit to just return the value without persisting.", + ), scope: scope.default("global"), projectPath, teamId, @@ -441,14 +553,32 @@ export function registerSecretTools(server: McpServer): void { server.tool( "entangle_secrets", - "[secrets] Link two keys so source updates/rotations propagate the same value to the target (mutates metadata; future writes sync). Reverse with disentangle_secrets without deleting values; do not confuse with set_secret (single-key write). Subject to tool policy.", + [ + "[secrets] Link two keys (across the same or different scopes) so future writes/rotations of either propagate the same value to the other.", + "Use when one logical credential lives under multiple names (e.g. `STRIPE_SECRET_KEY` global and project) and should never drift; prefer `set_secret` for unrelated values, and reverse the link with `disentangle_secrets` (does not delete values).", + "Mutates only the metadata of both envelopes — the values themselves are not changed by this call. Idempotent: re-running on an already-entangled pair is a no-op. Subject to tool policy. Returns a short confirmation: 'Entangled: SOURCE <-> TARGET'.", + ].join(" "), { - sourceKey: z.string().describe("Source secret key"), - targetKey: z.string().describe("Target secret key"), + sourceKey: z + .string() + .describe("First secret key in the pair. Example: 'STRIPE_SECRET_KEY'."), + targetKey: z + .string() + .describe("Second secret key to keep in lockstep with the source."), sourceScope: scope.default("global"), targetScope: scope.default("global"), - sourceProjectPath: z.string().optional(), - targetProjectPath: z.string().optional(), + sourceProjectPath: z + .string() + .optional() + .describe( + "Project root for sourceKey when sourceScope='project'. Defaults to the server cwd.", + ), + targetProjectPath: z + .string() + .optional() + .describe( + "Project root for targetKey when targetScope='project'. Defaults to the server cwd.", + ), }, async (params) => { const toolBlock = enforceToolPolicy( @@ -478,14 +608,24 @@ export function registerSecretTools(server: McpServer): void { server.tool( "disentangle_secrets", - "[secrets] Remove the sync link between two keys so rotations stop propagating. Does not delete either secret—use delete_secret to erase values. Contrast entangle_secrets (creates link). Safe if the link was already absent; updates metadata; subject to tool policy.", + [ + "[secrets] Break the sync link between two previously entangled keys so future rotations no longer propagate.", + "Use when one of the keys is being retired or should diverge intentionally; pair with `delete_secret` if you also want to erase one of the values, and use `entangle_secrets` to recreate the link.", + "Mutates only metadata; the current values remain untouched. Safe and idempotent — running on a pair that was never linked returns success without effect. Subject to tool policy. Returns 'Disentangled: SOURCE TARGET'.", + ].join(" "), { - sourceKey: z.string().describe("Source secret key"), - targetKey: z.string().describe("Target secret key"), + sourceKey: z.string().describe("First key in the previously linked pair."), + targetKey: z.string().describe("Second key in the previously linked pair."), sourceScope: scope.default("global"), targetScope: scope.default("global"), - sourceProjectPath: z.string().optional(), - targetProjectPath: z.string().optional(), + sourceProjectPath: z + .string() + .optional() + .describe("Project root for sourceKey when sourceScope='project'."), + targetProjectPath: z + .string() + .optional() + .describe("Project root for targetKey when targetScope='project'."), }, async (params) => { const toolBlock = enforceToolPolicy( diff --git a/src/mcp/tools/teleport.ts b/src/mcp/tools/teleport.ts index c2d80fc..ca7c78a 100644 --- a/src/mcp/tools/teleport.ts +++ b/src/mcp/tools/teleport.ts @@ -13,13 +13,23 @@ const { teamId, orgId, scope, projectPath } = commonSchemas; export function registerTeleportTools(server: McpServer): void { server.tool( "teleport_pack", - "[teleport] Pack secrets into an AES-256-GCM encrypted bundle for sharing between machines (quantum teleportation).", + [ + "[teleport] Encrypt one or more secrets into a single AES-256-GCM bundle string that can be safely transferred between machines.", + "Use to hand off a curated set of credentials to another developer or environment; prefer `export_secrets` for plaintext .env output (single machine, trusted) and `tunnel_create` for ephemeral one-shot delivery on the same machine.", + "Reads each secret value (records 'export' audit events) and produces a base64-encoded ciphertext. The bundle is unreadable without the same passphrase via `teleport_unpack`. Returns the bundle string directly. Errors with 'No secrets to pack' if the filter matched zero secrets.", + ].join(" "), { keys: z .array(z.string()) .optional() - .describe("Specific keys to pack (all if omitted)"), - passphrase: z.string().describe("Encryption passphrase"), + .describe( + "Whitelist of exact key names to include. Omit to pack every secret in the requested scope.", + ), + passphrase: z + .string() + .describe( + "Symmetric passphrase used to derive the AES-256-GCM key. The receiver must supply the same string to `teleport_unpack`. Pick something high-entropy and share it out-of-band.", + ), scope, projectPath, teamId, @@ -50,10 +60,22 @@ export function registerTeleportTools(server: McpServer): void { server.tool( "teleport_unpack", - "[teleport] Decrypt and import secrets from a teleport bundle.", + [ + "[teleport] Decrypt a bundle produced by `teleport_pack` and import each contained secret into the local keyring.", + "Use on the receiving machine after a packer hands you the bundle and passphrase out-of-band; prefer `dryRun=true` first to preview what will be written.", + "When dryRun is false this mutates the keyring (one 'write' event per imported secret) at the requested scope. Bad passphrase or tampered bundle returns JSON `{ ok: false, error: { message } }` with `isError: true`. On success returns 'Imported N secret(s) from teleport bundle'; in dryRun mode returns 'Would import N secrets:' followed by a `KEY [scope]` listing.", + ].join(" "), { - bundle: z.string().describe("Base64-encoded encrypted bundle"), - passphrase: z.string().describe("Decryption passphrase"), + bundle: z + .string() + .describe( + "Base64-encoded ciphertext returned by `teleport_pack`. Pass through whitespace untouched if possible.", + ), + passphrase: z + .string() + .describe( + "The same passphrase that was used to pack this bundle. Bad passphrases return an authentication error rather than wrong plaintext.", + ), scope: scope.default("global"), projectPath, teamId, @@ -62,7 +84,9 @@ export function registerTeleportTools(server: McpServer): void { .boolean() .optional() .default(false) - .describe("Preview without importing"), + .describe( + "If true, decrypt and report what would be written but do not mutate the keyring. Useful for verifying bundle contents before commit.", + ), }, async (params) => { const toolBlock = enforceToolPolicy("teleport_unpack", params.projectPath); diff --git a/src/mcp/tools/tooling.ts b/src/mcp/tools/tooling.ts index 903bcf7..cf55357 100644 --- a/src/mcp/tools/tooling.ts +++ b/src/mcp/tools/tooling.ts @@ -14,23 +14,42 @@ const { teamId, orgId, scope, projectPath } = commonSchemas; export function registerToolingTools(server: McpServer): void { server.tool( "exec_with_secrets", - "[exec] Run a shell command securely. Project secrets are injected into the environment, and any secret values in the output are automatically redacted to prevent leaking into transcripts.", + [ + "[exec] Run a child shell command with project secrets injected as environment variables and any leaked secret values redacted from captured stdout/stderr before they return to the agent.", + "Use to let an agent run a script that needs credentials (`npm run db:migrate`, `terraform plan`, `vercel deploy`) without ever putting plaintext values in the chat; prefer `env_generate` if you need to write a `.env` file to disk and `validate_secret` for upstream liveness checks.", + "Spawns a real child process — has whatever side effects the command itself causes (writes, network, exec). Subject to BOTH tool policy and exec policy (allowlist/denylist). Returns a text body with `Exit code: N` then `STDOUT:` and `STDERR:` blocks; both streams are scrubbed against the secret values that were injected.", + ].join(" "), { - command: z.string().describe("Command to run"), - args: z.array(z.string()).optional().describe("Command arguments"), + command: z + .string() + .describe( + "Executable name or full command to run. Example: 'pnpm', 'node', '/usr/bin/env'. Must be allowed by exec policy.", + ), + args: z + .array(z.string()) + .optional() + .describe( + "Positional arguments passed to `command`. Example: ['run', 'db:migrate']. Each element is passed verbatim with no extra shell parsing.", + ), keys: z .array(z.string()) .optional() - .describe("Only inject these specific keys"), + .describe( + "Whitelist of exact key names to inject. Omit to inject every secret in scope (subject to `tags`).", + ), tags: z .array(z.string()) .optional() - .describe("Only inject secrets with these tags"), + .describe( + "Inject only secrets carrying at least one of these tags. Combinable with `keys` as an AND filter.", + ), profile: z .enum(["unrestricted", "restricted", "ci"]) .optional() .default("restricted") - .describe("Exec profile: unrestricted, restricted, or ci"), + .describe( + "Exec sandbox profile. 'restricted' (default) limits PATH and inheritable env vars; 'ci' is restricted plus CI-friendly defaults (no TTY); 'unrestricted' inherits the full server environment — only pick this when you understand the leak risk.", + ), scope, projectPath, teamId, @@ -75,11 +94,17 @@ export function registerToolingTools(server: McpServer): void { server.tool( "scan_codebase_for_secrets", - "[scan] Scan a directory for hardcoded secrets using regex heuristics and Shannon entropy analysis. Returns file paths, line numbers, and the matched key/value to help migrate legacy codebases into q-ring.", + [ + "[scan] Walk a directory tree and flag plausible hardcoded secrets using regex heuristics plus Shannon-entropy scoring on string literals.", + "Use as a one-shot 'is anything leaking in this repo?' audit before commit/release; prefer `lint_files` when you already know the specific files to check (and want optional auto-fix).", + "Read-only — never modifies source files. Honors `.gitignore`. Returns JSON array of `{ file, line, key, value, kind }` findings, or 'No hardcoded secrets found in the specified directory.' when clean. False positives are possible — review before treating as ground truth.", + ].join(" "), { dirPath: z .string() - .describe("Absolute or relative path to the directory to scan"), + .describe( + "Directory to scan, absolute or relative to the server cwd. The scan recurses into subdirectories.", + ), }, async (params) => { const toolBlock = enforceToolPolicy("scan_codebase_for_secrets"); @@ -102,14 +127,24 @@ export function registerToolingTools(server: McpServer): void { server.tool( "lint_files", - "[scan] Scan specific files for hardcoded secrets. Optionally auto-fix by replacing them with process.env references and storing the values in q-ring.", + [ + "[scan] Inspect a specific list of files for hardcoded secrets and, when `fix` is true, replace each finding with `process.env.KEY` while storing the extracted value into the keyring.", + "Use to migrate a known set of files (e.g. just-changed files in a pre-commit hook) into q-ring; prefer `scan_codebase_for_secrets` for a whole-tree audit and `import_dotenv` to ingest an existing .env.", + "With `fix: false` this is read-only. With `fix: true` this MUTATES the listed source files in place (review with git diff!) and writes one new secret per finding to the keyring. Returns a JSON array of `{ file, line, key, value, kind }` findings, or 'No hardcoded secrets found in the specified files.'.", + ].join(" "), { - files: z.array(z.string()).describe("File paths to lint"), + files: z + .array(z.string()) + .describe( + "Absolute or relative paths to lint. Non-existent paths surface as scan errors.", + ), fix: z .boolean() .optional() .default(false) - .describe("Auto-replace and store secrets"), + .describe( + "If true, rewrite the source files to read `process.env.KEY` and store the extracted value in the keyring. If false (default), only report findings.", + ), scope, projectPath, teamId, @@ -140,7 +175,11 @@ export function registerToolingTools(server: McpServer): void { server.tool( "analyze_secrets", - "[agent] Analyze secret usage patterns and provide optimization suggestions including most accessed, stale, unused, and rotation recommendations.", + [ + "[agent] Cross-reference the secrets in scope with recent audit events to produce a usage profile and rotation/retirement suggestions.", + "Use as a quarterly hygiene check or as input to a planner that decides what to rotate or delete; prefer `health_check` for decay-only triage and `audit_log` to inspect access timelines for one key.", + "Read-only; uses the most recent ~500 audit events. Returns JSON `{ total, expired, stale, neverAccessed: [...], noRotationFormat: [...], mostAccessed: [{ key, reads }] }`. `neverAccessed` and `noRotationFormat` are good candidates for cleanup or for adding rotation hints.", + ].join(" "), { scope, projectPath, @@ -189,9 +228,19 @@ export function registerToolingTools(server: McpServer): void { server.tool( "status_dashboard", - "[dashboard] Launch the quantum status dashboard — a local SSE-driven web page showing live KPIs (secrets, env, protected, approvals, hooks, 24h reads, anomalies), health summary, environment, .q-ring.json manifest gaps, governance policy summary, sortable searchable secrets table, decay/superposition/entanglement/tunnel cards, active approvals & hooks, agent memory, anomaly alerts, and a filterable 24h audit feed. Returns the URL to open in a browser. Never exposes secret values.", + [ + "[dashboard] Start a local web dashboard (`http://127.0.0.1:PORT`) that streams live KPIs, secret tables, manifest gaps, hooks, audit events, and anomalies via Server-Sent Events.", + "Use when an operator (or an agent on behalf of one) wants a richer visual surface than chat output; prefer `health_check` / `analyze_secrets` for one-shot text summaries inside the conversation.", + "Side effect: binds an HTTP server on the requested port (one process-wide instance — re-running returns the existing URL instead of starting a second server). Never exposes secret values. Returns the URL string to open in a browser.", + ].join(" "), { - port: z.number().optional().default(9876).describe("Port to serve on"), + port: z + .number() + .optional() + .default(9876) + .describe( + "TCP port to listen on (default 9876). Pick another port if 9876 is already in use; the call fails if binding errors.", + ), }, async (params) => { const toolBlock = enforceToolPolicy("status_dashboard"); @@ -214,17 +263,25 @@ export function registerToolingTools(server: McpServer): void { server.tool( "agent_scan", - "[agent] Multi-project health pass: decay, staleness, audit anomalies, manifest gaps; returns JSON. Prefer health_check for a read-only scoped decay/anomaly text summary (no writes). Prefer detect_anomalies for audit-pattern spikes on one key. With autoRotate=true, overwrites expired secret values in the keyring (credential change—not undoable); leave false unless intentional rotation. Same policy gates as other MCP tools; no separate external auth.", + [ + "[agent] Run a multi-project health pass that gathers decay status, audit anomalies, and `.q-ring.json` manifest gaps across one or more project paths and (optionally) auto-rotates expired secrets with freshly generated values.", + "Use as the canonical 'agent maintenance loop' across a portfolio of repos; prefer `health_check` for a single read-only scope, `detect_anomalies` for audit-only triage, and `check_project` for a single-project manifest check.", + "With `autoRotate=false` (default) this is read-only. With `autoRotate=true` it OVERWRITES expired secret values in the keyring with generated replacements — credential changes that may break upstream integrations until they are propagated. Subject to tool policy. Returns a JSON report of per-project findings and any rotations performed.", + ].join(" "), { autoRotate: z .boolean() .optional() .default(false) - .describe("Auto-rotate expired secrets with generated values"), + .describe( + "If true, replace expired secrets with newly generated values (using each secret's `rotationFormat`/`rotationPrefix`). Only enable when intentional rotation is desired — this is destructive on the upstream side.", + ), projectPaths: z .array(z.string()) .optional() - .describe("Project paths to monitor"), + .describe( + "List of absolute project roots to scan. Defaults to `[server.cwd]` when omitted.", + ), }, async (params) => { const toolBlock = enforceToolPolicy("agent_scan"); diff --git a/src/mcp/tools/tunnel.ts b/src/mcp/tools/tunnel.ts index eb9fba1..ed88461 100644 --- a/src/mcp/tools/tunnel.ts +++ b/src/mcp/tools/tunnel.ts @@ -11,11 +11,29 @@ import { text, enforceToolPolicy } from "./_shared.js"; export function registerTunnelTools(server: McpServer): void { server.tool( "tunnel_create", - "[tunnel] Create an ephemeral secret that exists only in memory (quantum tunneling). Never persisted to disk. Optional TTL and max-reads for self-destruction.", + [ + "[tunnel] Stash a one-shot or short-lived secret in the q-ring server's process memory and return an ID that can be used to read it back.", + "Use for handing a one-time value to another tool/process without persisting it (npm OTP codes, magic-link tokens, copy/paste between machines via a relay); prefer `set_secret` with `ttlSeconds` when you actually want a tracked, auditable secret.", + "Mutates only in-memory state — the value never touches disk and is lost on server restart. Subject to tool policy. Returns JSON `{ ok, data: { id } }` where `id` is an opaque string to pass to `tunnel_read`/`tunnel_destroy`.", + ].join(" "), { - value: z.string().describe("The secret value"), - ttlSeconds: z.number().optional().describe("Auto-expire after N seconds"), - maxReads: z.number().optional().describe("Self-destruct after N reads"), + value: z + .string() + .describe( + "The plaintext value to tunnel. Held only in process memory; never logged.", + ), + ttlSeconds: z + .number() + .optional() + .describe( + "Auto-destroy the tunnel after this many seconds. Omit for no time limit (then a `maxReads` is highly recommended).", + ), + maxReads: z + .number() + .optional() + .describe( + "Self-destruct after this many successful `tunnel_read` calls. Use 1 for true one-shot delivery.", + ), }, async (params) => { const toolBlock = enforceToolPolicy("tunnel_create"); @@ -31,9 +49,17 @@ export function registerTunnelTools(server: McpServer): void { server.tool( "tunnel_read", - "[tunnel] Read an ephemeral tunneled secret by ID. May self-destruct if max-reads is reached.", + [ + "[tunnel] Fetch the value stashed by a prior `tunnel_create` call by its ID.", + "Use exactly once per intended consumer; the value is destructive-by-design and may self-delete after this call.", + "Increments the read counter and may auto-destroy the tunnel if `maxReads` was set. Returns JSON `{ ok, data: { id, value } }` on success, or an error 'Tunnel \"...\" not found or expired' if the tunnel has been destroyed, hit its TTL, or never existed.", + ].join(" "), { - id: z.string().describe("Tunnel ID"), + id: z + .string() + .describe( + "The opaque tunnel ID returned by `tunnel_create`. Case-sensitive.", + ), }, async (params) => { const toolBlock = enforceToolPolicy("tunnel_read"); @@ -51,7 +77,11 @@ export function registerTunnelTools(server: McpServer): void { server.tool( "tunnel_list", - "[tunnel] List active tunneled secrets (IDs and metadata only, never values).", + [ + "[tunnel] Enumerate all currently-active tunnels in the q-ring server with their remaining read budget and time-to-live.", + "Use to audit what is still in memory or to look up an ID you forgot; values are never included in the output.", + "Read-only. Returns one line per tunnel formatted as `id | reads:N | max:N | expires:Ns`, or the literal text 'No active tunnels' when the list is empty.", + ].join(" "), {}, async () => { const toolBlock = enforceToolPolicy("tunnel_list"); @@ -80,9 +110,13 @@ export function registerTunnelTools(server: McpServer): void { server.tool( "tunnel_destroy", - "[tunnel] Immediately destroy a tunneled secret.", + [ + "[tunnel] Immediately remove a tunnel from memory, regardless of remaining reads or TTL.", + "Use when a tunneled value should be cancelled before delivery (e.g. wrong recipient, secret already rotated); prefer letting `maxReads`/TTL handle cleanup for normal flows.", + "Mutates in-memory state only. Returns 'Destroyed ID' on success or a not-found error if the ID is unknown or already gone.", + ].join(" "), { - id: z.string().describe("Tunnel ID"), + id: z.string().describe("The opaque tunnel ID to destroy."), }, async (params) => { const toolBlock = enforceToolPolicy("tunnel_destroy"); diff --git a/src/mcp/tools/validation.ts b/src/mcp/tools/validation.ts index 2e2328e..2de5911 100644 --- a/src/mcp/tools/validation.ts +++ b/src/mcp/tools/validation.ts @@ -20,13 +20,23 @@ const { teamId, orgId, scope, projectPath } = commonSchemas; export function registerValidationTools(server: McpServer): void { server.tool( "validate_secret", - "[validation] Test if a secret is actually valid with its target service (e.g., OpenAI, Stripe, GitHub). Uses provider auto-detection based on key prefixes, or accepts an explicit provider name. Never logs the secret value.", + [ + "[validation] Test whether a stored secret is still accepted by its upstream service (OpenAI, Stripe, GitHub, AWS, generic HTTP, etc.) by making a minimal authenticated request.", + "Use to confirm liveness before relying on a credential or as the verification step after `rotate_secret`; prefer `ci_validate_secrets` for a batch run across every key in scope.", + "Side effects: makes one outbound network request per call (may incur tiny provider-side rate-limit cost). Records 'read' for the underlying secret value in the audit log; the value itself is never logged. Returns JSON `{ valid, provider, status?, message?, rateLimit?, ... }` (provider-specific shape).", + ].join(" "), { - key: z.string().describe("The secret key name"), + key: z + .string() + .describe( + "The exact key whose value should be tested upstream. Example: 'OPENAI_API_KEY'.", + ), provider: z .string() .optional() - .describe("Force a specific provider (openai, stripe, github, aws, http)"), + .describe( + "Force a specific provider id. Built-ins include 'openai', 'stripe', 'github', 'aws', 'http'. Omit to auto-detect from the value's prefix or the secret's stored provider hint.", + ), scope, projectPath, teamId, @@ -49,7 +59,11 @@ export function registerValidationTools(server: McpServer): void { server.tool( "list_providers", - "[validation] List all available validation providers for secret liveness testing.", + [ + "[validation] Enumerate the secret-validation providers q-ring knows how to call (OpenAI, Stripe, GitHub, …) along with their auto-detect prefixes.", + "Use to discover what `provider` string to pass to `validate_secret`/`rotate_secret`, or to check whether your custom provider is registered.", + "Read-only. Returns JSON array of `{ name, description, prefixes }` objects. `prefixes` are the literal key-value prefixes (e.g. 'sk-' for OpenAI) used for auto-detection.", + ].join(" "), {}, async () => { const toolBlock = enforceToolPolicy("list_providers"); @@ -66,10 +80,21 @@ export function registerValidationTools(server: McpServer): void { server.tool( "rotate_secret", - "[validation] Attempt issuer-native rotation of a secret via its detected or specified provider. Returns rotation result.", + [ + "[validation] Ask the upstream provider to issue a fresh credential for this secret and store the new value back into the keyring.", + "Use when a secret is expiring, leaked, or part of a scheduled rotation; prefer `generate_secret` for self-managed values you fully control, and `agent_scan --autoRotate` for sweep-style rotation across multiple expired keys.", + "Mutates the keyring with the newly-issued value if rotation succeeds (one 'write' audit event), and makes outbound network requests against the provider's rotation API. Returns JSON `{ rotated, newValue?, message?, ... }`. If `rotated` is false, the existing value is left untouched.", + ].join(" "), { - key: z.string().describe("The secret key to rotate"), - provider: z.string().optional().describe("Force a specific provider"), + key: z + .string() + .describe("Exact key to rotate. Must already exist in the keyring."), + provider: z + .string() + .optional() + .describe( + "Force a specific provider id (see `list_providers`). Omit to auto-detect from the current value or the secret's stored provider hint.", + ), scope, projectPath, teamId, @@ -96,7 +121,11 @@ export function registerValidationTools(server: McpServer): void { server.tool( "ci_validate_secrets", - "[validation] CI-oriented batch validation: validates all accessible secrets against their providers and returns a structured pass/fail report.", + [ + "[validation] Validate every accessible secret in the requested scope against its detected provider in a single batch and return a structured pass/fail report.", + "Use as a CI gate ('do all our credentials still work before deploy?') or as a pre-rotation health pass; prefer `validate_secret` for a single key.", + "Side effects: one outbound request per validatable secret (cost scales with N). Reads each secret value (records 'read' audit events). Returns JSON `{ total, valid, invalid, results: [...] }` listing per-key status, provider, and error messages where applicable. Returns 'No secrets to validate' if nothing in scope has a provider mapping.", + ].join(" "), { scope, projectPath, From bd87695144fcf4464d4b80c722ed221c7bb3ae70 Mon Sep 17 00:00:00 2001 From: I4cTime Date: Thu, 25 Jun 2026 00:14:27 -0500 Subject: [PATCH 02/16] =?UTF-8?q?v0.12.0=20=E2=80=94=20security=20hardenin?= =?UTF-8?q?g,=20dashboard=20rebuild=20(Preact),=20dependency=20patches=20(?= =?UTF-8?q?#58)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: v0.12.0 — security hardening, dashboard rebuild, dependency patches Security - export_secrets now honors the requiresApproval gate on bulk reads (was bypassable over MCP); skips protected keys lacking a valid grant. - Governance policy is anchored to the MCP server's launch CWD (setPolicyRoot) so an agent can't escape .q-ring.json restrictions via a crafted projectPath. - import_dotenv treats input as literal content for MCP/agent/api callers (file-path convenience is CLI-only), closing a local-file-read vector. - SSRF protection re-validates the resolved IP at connect time (guardedLookup), closing the DNS-rebinding TOCTOU window. - Teleport PBKDF2 raised 100k -> 210k (SHA-512, OWASP); bundles record `iter` so legacy bundles still decrypt. - Status dashboard endpoints require a per-launch, timing-safe token. - Hardened exec network detection (basename) and checkExecPolicy (token/path boundary, not substring); policy cache invalidates on .q-ring.json mtime. - Fixed dotenv inline-comment parsing and the password class guarantee. - New regression tests (176 total). Dashboard - Rebuilt the live status dashboard on Preact + htm with VDOM diffing, so the SSE stream updates data in place: entrance animations run once instead of replaying every 5s (the flicker/"reload" bug), and focus/scroll are preserved. - Still a single self-contained, offline, CSP-clean page: the runtime + app are esbuild-bundled (src/dashboard-client/main.js -> src/core/dashboard-client.ts via scripts/build-dashboard-client.mjs, run by `build`) and inlined. Dependencies - Cleared 23 Dependabot alerts via pnpm overrides (hono, qs, ip-address, fast-uri, esbuild, postcss) + vite pinned as a devDependency. - Added preact/htm/esbuild as devDependencies only; no new runtime deps. Breaking - Dashboard returns 403 without the token; use the printed tokenized URL. - Single MCP server spanning multiple project roots now applies only the launch directory's policy. Co-Authored-By: Claude Opus 4.8 * fix(ci): pin pnpm, trim bleeding-edge deps to satisfy lockfile policy CI ran pnpm@latest (v11), which ships a default minimumReleaseAge supply-chain policy and no longer reads `pnpm.overrides` from package.json — diverging from the local pnpm 10 and failing `--frozen-lockfile` on 18 freshly-published lockfile entries. - Pin pnpm via `packageManager: pnpm@10.32.1`; CI reads it (drop `version: latest`) so resolution matches local exactly. Add pnpm cache to setup-node. - Pin `vite` to 8.0.16 (was floating to 8.1.0, which drags in the just- published rolldown 1.1.3 native-binary stack); 8.0.16 uses aged rolldown 1.0.3 and still fixes the vite advisory. - Pin the `qs` override to 6.15.2 (the patched, aged release) instead of floating to the same-day 6.15.3. - Regenerate a lean lockfile from the develop baseline (no --force churn). Local parity with CI: install --frozen-lockfile, audit --audit-level=high (0; 2 dev-only moderates remain), typecheck, lint, build, 176 tests — all green. Co-Authored-By: Claude Opus 4.8 * ci: cancel superseded runs on the same ref Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- .cursor-plugin/marketplace.json | 2 +- .github/workflows/ci.yml | 14 +- CHANGELOG.md | 28 + README.md | 24 +- SECURITY.md | 9 +- cursor-plugin/.cursor-plugin/plugin.json | 2 +- docs/cli-mcp-parity.md | 11 +- eslint.config.mjs | 17 + package.json | 19 +- pnpm-lock.yaml | 550 +++++++++-------- scripts/build-dashboard-client.mjs | 53 ++ src/__tests__/core/import.test.ts | 7 + src/__tests__/core/noise.test.ts | 15 + src/__tests__/core/policy-root.test.ts | 45 ++ src/__tests__/core/policy.test.ts | 45 +- src/__tests__/core/ssrf.test.ts | 37 +- src/__tests__/core/teleport.test.ts | 43 ++ src/cli/commands/tooling.ts | 3 +- src/core/dashboard-client.ts | 5 + src/core/dashboard-html.ts | 736 +---------------------- src/core/dashboard.ts | 30 +- src/core/exec.ts | 13 +- src/core/import.ts | 24 +- src/core/keyring.ts | 18 + src/core/noise.ts | 42 +- src/core/policy.ts | 49 +- src/core/ssrf.ts | 51 ++ src/core/teleport.ts | 21 +- src/dashboard-client/main.js | 665 ++++++++++++++++++++ src/mcp/server.ts | 6 + src/mcp/tools/tooling.ts | 6 +- src/utils/http-request.ts | 10 +- 32 files changed, 1554 insertions(+), 1046 deletions(-) create mode 100644 scripts/build-dashboard-client.mjs create mode 100644 src/__tests__/core/policy-root.test.ts create mode 100644 src/core/dashboard-client.ts create mode 100644 src/dashboard-client/main.js diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 2a6022b..3c2e7e7 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -12,7 +12,7 @@ "name": "qring", "source": "cursor-plugin", "description": "Quantum keyring for AI agents — manage secrets, scan for leaks, rotate keys, and enforce policy directly from Cursor.", - "version": "0.11.7", + "version": "0.12.0", "keywords": [ "secrets", "keyring", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a03699c..583109e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,19 +9,25 @@ on: permissions: contents: read +# Cancel superseded runs on the same ref (e.g. rapid PR pushes) to save minutes. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + # pnpm version is pinned via the "packageManager" field in package.json so + # CI resolves identically to local (no surprise pnpm-latest policy drift). + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 with: node-version: "22" - - - uses: pnpm/action-setup@v4 - with: - version: latest + cache: pnpm - run: pnpm install --frozen-lockfile diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b95921..a4fa4ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,34 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.12.0] — 2026-06-24 + +### Security +- **`export_secrets` now honors the approval gate** — bulk reads previously collapsed and returned approval-protected values (`requiresApproval`) without an approval grant, even though `get_secret` enforced the gate. Export over MCP now skips protected secrets that lack a valid approval and records a blocked-read audit event. CLI exports (`source: "cli"`) are unaffected. +- **Governance policy is anchored to the server's launch directory** — MCP callers supply `projectPath` freely, so resolving `.q-ring.json` policy from it let a malicious/compromised agent escape `denyTools` / `deniedKeys` / `deniedTags` by pointing at a directory with no policy. The MCP server now pins policy resolution to its startup CWD (`setPolicyRoot`). Project-scoped *secret* resolution still follows `projectPath`; only policy is pinned. +- **`import_dotenv` no longer reads arbitrary files over MCP** — the importer's path-or-content convenience (read from disk if the argument is a path) is now restricted to the trusted local CLI. MCP/agent/api callers always treat the argument as literal `.env` content, closing a local-file-disclosure vector (e.g. passing `~/.aws/credentials` to be parsed and re-read via `get_secret`). +- **SSRF protection now validates at connect time (DNS-rebinding guard)** — outbound HTTP(S) (validation providers, webhook hooks) re-checks the resolved IP via a guarded `lookup` when the socket connects, not only up front. This closes the TOCTOU window where a hostname passed `checkSSRF` then resolved to a private/loopback address. Honors `Q_RING_ALLOW_PRIVATE_HOOKS=1`. +- **Teleport key derivation strengthened** — PBKDF2-HMAC-SHA512 iterations raised from 100 000 to 210 000 (OWASP 2023 floor). Bundles now record their `iter` count, so existing (pre-bump) bundles still decrypt. +- **Status dashboard endpoints require a per-launch token** — the dashboard binds to `127.0.0.1`, but on a shared host any local user/process could read key names, the audit log, and approval grants. Every route now requires a random, timing-safe token (`?token=…`), surfaced in the URL printed by `qring status` and the `status_dashboard` MCP tool. + +### Changed +- **Status dashboard rebuilt on Preact + htm** — the live page is now a component-based app with VDOM diffing instead of full-`innerHTML` re-rendering. The SSE stream updates data in place, so card entrance animations run once on mount rather than replaying on every 5-second tick (the old "reload/flicker on update" behavior), and input focus/caret and scroll positions are preserved for free. Still a single self-contained, offline, CSP-clean page: the Preact + htm runtime and the app are bundled (esbuild) into an inlined script via `src/dashboard-client/main.js` → `src/core/dashboard-client.ts` (generated by `pnpm run build:dashboard`, run automatically as part of `build`). +- **Exec network/command guards hardened** — `allowNetwork:false` now matches on the command basename (catches `/usr/bin/curl`), and `policy.exec.denyCommands` uses token/path-boundary matching instead of bare substring (denying `rm` no longer trips `charm`). +- **Policy cache invalidates on `.q-ring.json` change** — `loadPolicy` keys its cache on the config file's mtime, so a long-lived MCP server picks up edited policy without a restart. + +### Fixed +- **dotenv inline-comment parsing** — an unquoted `#` is only treated as a comment when preceded by whitespace, so values like `COLOR=#ff0000` are preserved. +- **`generate_secret --format password` class guarantee** — passwords are now built from one guaranteed character per class and shuffled (CSPRNG), eliminating the rare case where the previous fixup pass overwrote the sole representative of a class. + +### Dependencies +- **Cleared 23 open Dependabot alerts** (4 high / 17 medium / 2 low) by raising `pnpm.overrides` to patched transitive versions: `hono` ≥4.12.25, `qs` ≥6.15.2, `ip-address` ≥10.1.1, `fast-uri` ≥3.1.2 (`<4`), `esbuild` ≥0.28.1, `postcss` ≥8.5.10. Pinned `vite` ≥8.0.16 as an explicit `devDependency` (overrides don't apply to peer-only deps). All flagged packages are transitive via `@modelcontextprotocol/sdk`'s Express/Hono/ajv HTTP stack (runtime) or vitest/tsup (dev); q-ring uses the stdio MCP transport only, so the runtime ones were never on an executed code path. +- **Added dev dependencies for the dashboard build** — `preact`, `htm`, and `esbuild` (devDependencies only; the runtime is bundled and inlined, so the published package gains no new runtime dependencies — it still ships only `@modelcontextprotocol/sdk`, `@napi-rs/keyring`, `commander`, `zod`). + +### Notes +- **Breaking (dashboard):** the dashboard now returns `403` without the token. Bookmarked `http://127.0.0.1:9876` URLs must be replaced with the tokenized URL printed at launch. +- **Behavioral (governance):** if you run a single MCP server across multiple project roots with per-project policies, only the launch directory's `.q-ring.json` policy now applies. +- Version propagated to the Cursor plugin, marketplace metadata, and SECURITY.md via `pnpm run sync-versions`. + ## [0.11.7] — 2026-04-27 ### Changed diff --git a/README.md b/README.md index 759e11a..6113112 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ qring tunnel list ### Teleportation — Encrypted Sharing -Pack secrets into AES-256-GCM encrypted bundles for secure transfer between machines. +Pack secrets into AES-256-GCM encrypted bundles for secure transfer between machines. Keys are derived with PBKDF2-HMAC-SHA512 (210 000 iterations) from your passphrase; each bundle records its iteration count, so bundles produced by older versions still unpack. ```bash # Pack secrets (prompts for passphrase) @@ -197,7 +197,7 @@ qring teleport unpack --dry-run ### Import — Bulk Secret Ingestion -Import secrets from `.env` files directly into q-ring. Supports standard dotenv syntax including comments, quoted values, and escape sequences. +Import secrets from `.env` files directly into q-ring. Supports standard dotenv syntax including comments, quoted values, and escape sequences. The CLI accepts either a file path or raw content; the `import_dotenv` MCP tool only accepts raw content (it never reads files from disk) so an agent can't coerce it into reading arbitrary local files. ```bash # Import all secrets from a .env file @@ -335,7 +335,7 @@ qring hook test Hooks are fire-and-forget: a failing hook never blocks secret operations. The hook registry is stored at `~/.config/q-ring/hooks.json`. -**SSRF protection:** HTTP hook URLs targeting private/loopback IP ranges (`127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `::1`, `fc00::/7`) are blocked by default. DNS resolution is checked before the request is sent. To allow hooks targeting local services (e.g. during development), set the environment variable `Q_RING_ALLOW_PRIVATE_HOOKS=1`. +**SSRF protection:** HTTP hook URLs targeting private/loopback IP ranges (`127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `::1`, `fc00::/7`) are blocked by default. DNS is checked up front *and* re-validated at connect time, so a hostname can't pass the check then rebind to a private address before the socket opens. To allow hooks targeting local services (e.g. during development), set the environment variable `Q_RING_ALLOW_PRIVATE_HOOKS=1`. ### Configurable Rotation @@ -397,7 +397,7 @@ qring get DB_URL ### User Approvals (Zero-Trust Agent) -Protect sensitive production secrets from being read autonomously by the MCP server without explicit user approval. Each approval token is HMAC-verified, scoped, reasoned, and time-limited. +Protect sensitive production secrets from being read autonomously by the MCP server without explicit user approval. Each approval token is HMAC-verified, scoped, reasoned, and time-limited. The gate applies to bulk reads too — `export_secrets` and `teleport_pack` over MCP skip approval-protected keys that lack a valid grant. ```bash # Mark a secret as requiring approval @@ -508,6 +508,8 @@ qring wizard myservice --hook-exec "pm2 restart app" Define project-level governance rules in `.q-ring.json` to control which MCP tools can be used, which keys are accessible, and which commands can be executed. Policy is enforced at both the MCP server and keyring level. +Over MCP, policy is resolved from the directory the server was **launched** in — not from the `projectPath` a caller passes — so an agent can't sidestep restrictions by pointing at a directory with no policy. Launch the MCP server from your project root (where `.q-ring.json` lives). Edits to `.q-ring.json` are picked up automatically (the policy cache invalidates on file change), so you don't need to restart the server. + ```bash # View the active policy qring policy @@ -626,7 +628,7 @@ qring agent --once ### Quantum Status Dashboard — Live Monitoring -Launch a real-time dashboard in your browser that turns the entire quantum subsystem into one glanceable page. The dashboard is a single self-contained HTML page served locally — no dependencies, no cloud, no config — and streams updates every 5 seconds via Server-Sent Events while preserving search input and scroll position across ticks. +Launch a real-time dashboard in your browser that turns the entire quantum subsystem into one glanceable page. It's a single self-contained HTML page served locally — no cloud, no config, fully offline — built as a Preact + htm app (runtime bundled and inlined). It streams updates every 5 seconds via Server-Sent Events and diffs the DOM in place, so data refreshes without re-running entrance animations and your search input, caret, and scroll position are preserved across ticks. What you get: @@ -644,14 +646,16 @@ What you get: Top-bar controls let you **pause** SSE updates (handy while reading the audit feed), **refresh** on demand, or jump to the raw JSON snapshot at `/api/status`. Keyboard shortcuts: `/` focus secrets search · `P` pause · `R` refresh. +The dashboard binds to `127.0.0.1` only and **never** exposes secret values, but it does surface key names, the audit log, and approval grants — so every route is gated by a random, per-launch token. `qring status` prints (and opens) the full URL including `?token=…`; requests without the token get a `403`. Stop the server to invalidate the token. + ```bash -# Open the dashboard (auto-launches your browser) +# Open the dashboard (auto-launches your browser at http://127.0.0.1:9876/?token=…) qring status # Specify a custom port qring status --port 4200 -# Don't auto-open the browser +# Don't auto-open the browser (copy the printed tokenized URL yourself) qring status --no-open ``` @@ -668,8 +672,8 @@ q-ring includes a full MCP server with 44 tools for AI agent integration. | `set_secret` | Create or overwrite a single secret with optional TTL, per-env state, tags, rotation format | | `delete_secret` | Permanently remove a secret value (not undoable from q-ring) | | `has_secret` | Boolean existence check that respects decay (no audit read) | -| `export_secrets` | Render multiple secrets as `.env` or JSON for one-off export | -| `import_dotenv` | Parse `.env` text and bulk-store every key/value pair | +| `export_secrets` | Render multiple secrets as `.env` or JSON for one-off export (skips approval-protected keys without a grant) | +| `import_dotenv` | Parse `.env` text and bulk-store every key/value pair (accepts raw content only — never reads files) | | `check_project` | Compare `.q-ring.json` manifest against the keyring for missing/expired/stale keys | | `env_generate` | Render a complete `.env` body from the project manifest, with warnings for gaps | @@ -741,7 +745,7 @@ q-ring includes a full MCP server with 44 tools for AI agent integration. | `verify_audit_chain` | Recompute the audit hash chain and report the first break point if tampered | | `export_audit` | Export audit events as jsonl, json, or csv for archival/SIEM | | `health_check` | Read-only scope sweep: decay/stale/expired counts plus current anomalies | -| `status_dashboard` | Start a local SSE dashboard with live KPIs, secrets, hooks, and audit feed | +| `status_dashboard` | Start a local SSE dashboard with live KPIs, secrets, hooks, and audit feed (returns a token-gated `127.0.0.1` URL) | | `agent_scan` | Multi-project health pass with optional `autoRotate` for expired secrets | ### Governance & Policy Tools diff --git a/SECURITY.md b/SECURITY.md index b2fd56d..c1ef67e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,8 @@ | Version | Supported | |---------|-----------| -| 0.11.x | Yes | -| < 0.11 | No | +| 0.12.x | Yes | +| < 0.12 | No | ## Reporting a Vulnerability @@ -50,7 +50,10 @@ The following areas are in scope for security reports: - **Tunneling** — leaks of ephemeral in-memory secrets to disk or logs. - **Teleportation** — weaknesses in the encrypted sharing protocol. - **Entanglement** — unintended exposure during linked secret rotation. -- **MCP server** — unauthorized access to secrets via the MCP transport. +- **MCP server** — unauthorized access to secrets via the MCP transport, including bypasses of the approval gate, governance policy, or per-key restrictions. +- **Governance & approvals** — escaping `.q-ring.json` policy, forging/bypassing approval tokens, or reading protected secrets via bulk operations (`export_secrets`, `teleport_pack`). +- **Outbound requests** — SSRF via hook/validation URLs (including DNS-rebinding) to private/loopback ranges. +- **Status dashboard** — unauthorized access to the local dashboard, its `/api/status` snapshot, or SSE stream. - **CLI** — command injection, argument parsing flaws, or privilege escalation. ## Out of Scope diff --git a/cursor-plugin/.cursor-plugin/plugin.json b/cursor-plugin/.cursor-plugin/plugin.json index b6ed82e..efae633 100644 --- a/cursor-plugin/.cursor-plugin/plugin.json +++ b/cursor-plugin/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "qring", "description": "Quantum keyring for AI agents — manage secrets, scan for leaks, rotate keys, and enforce policy directly from Cursor.", - "version": "0.11.7", + "version": "0.12.0", "author": { "name": "I4cTime" }, diff --git a/docs/cli-mcp-parity.md b/docs/cli-mcp-parity.md index 97c02ac..43a1207 100644 --- a/docs/cli-mcp-parity.md +++ b/docs/cli-mcp-parity.md @@ -17,10 +17,15 @@ approval gates. `package.json` next to `dist/`). - **Policy enforcement:** Every MCP tool runs `enforceToolPolicy` before executing; the CLI goes through the same `checkToolPolicy`, `checkExecPolicy`, - and `checkKeyReadPolicy` helpers in `src/core/policy.ts`. + and `checkKeyReadPolicy` helpers in `src/core/policy.ts`. **Policy root + differs by surface:** the MCP server pins policy resolution to its launch CWD + (`setPolicyRoot`, called in `src/mcp/server.ts`) so an agent can't escape + governance via a crafted `projectPath`; the CLI resolves policy from the + caller's `projectPath`/CWD as usual. - **SSRF protection:** All outbound HTTP (hooks, JIT providers, provider - validation) routes through `src/utils/http-request.ts` and, for - user-configurable URLs, `src/core/ssrf.ts`. + validation) routes through `src/utils/http-request.ts`, which re-validates the + resolved IP at connect time via `guardedLookup`; user-configurable URLs are + additionally pre-checked through `src/core/ssrf.ts`. ## Command ↔ tool map diff --git a/eslint.config.mjs b/eslint.config.mjs index 1341853..f771d7f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -7,6 +7,23 @@ export default tseslint.config( { ignores: ["dist/**", "node_modules/**", "coverage/**"], }, + { + // Browser client for the status dashboard (bundled into the served HTML). + files: ["src/dashboard-client/**/*.js"], + languageOptions: { + globals: { + window: "readonly", + document: "readonly", + console: "readonly", + fetch: "readonly", + EventSource: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + }, + }, + }, { rules: { "@typescript-eslint/no-unused-vars": [ diff --git a/package.json b/package.json index 82d462b..225f44e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@i4ctime/q-ring", - "version": "0.11.7", + "version": "0.12.0", "mcpName": "io.github.I4cTime/q-ring", "description": "Quantum keyring for AI coding tools — Cursor, Kiro, Claude Code. Secrets, superposition, entanglement, MCP.", "type": "module", @@ -16,7 +16,8 @@ "LICENSE" ], "scripts": { - "build": "tsup", + "build": "node scripts/build-dashboard-client.mjs && tsup", + "build:dashboard": "node scripts/build-dashboard-client.mjs", "dev": "tsup --watch", "typecheck": "tsc --noEmit", "lint": "eslint src --max-warnings 0", @@ -40,6 +41,7 @@ "api-keys" ], "license": "AGPL-3.0-only", + "packageManager": "pnpm@10.32.1", "engines": { "node": ">=18" }, @@ -60,20 +62,29 @@ "devDependencies": { "@eslint/js": "^9.39.2", "@types/node": "^25.5.0", + "esbuild": "^0.28.1", "eslint": "^9.39.2", + "htm": "^3.1.1", + "preact": "^10.29.2", "prettier": "^3.7.4", "tsup": "^8.5.1", "typescript": "^5.9.3", "typescript-eslint": "^8.50.1", + "vite": "8.0.16", "vitest": "^4.1.1" }, "pnpm": { "overrides": { "picomatch": ">=4.0.4", "path-to-regexp": ">=8.4.0", - "hono": ">=4.12.12", + "hono": ">=4.12.25", "@hono/node-server": ">=1.19.13", - "vite": ">=8.0.5" + "vite": "8.0.16", + "qs": "6.15.2", + "ip-address": ">=10.1.1", + "fast-uri": ">=3.1.2 <4", + "esbuild": ">=0.28.1", + "postcss": ">=8.5.10" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f120184..431e39a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,9 +7,14 @@ settings: overrides: picomatch: '>=4.0.4' path-to-regexp: '>=8.4.0' - hono: '>=4.12.12' + hono: '>=4.12.25' '@hono/node-server': '>=1.19.13' - vite: '>=8.0.5' + vite: 8.0.16 + qs: 6.15.2 + ip-address: '>=10.1.1' + fast-uri: '>=3.1.2 <4' + esbuild: '>=0.28.1' + postcss: '>=8.5.10' importers: @@ -34,188 +39,200 @@ importers: '@types/node': specifier: ^25.5.0 version: 25.5.2 + esbuild: + specifier: '>=0.28.1' + version: 0.28.1 eslint: specifier: ^9.39.2 version: 9.39.4 + htm: + specifier: ^3.1.1 + version: 3.1.1 + preact: + specifier: ^10.29.2 + version: 10.29.2 prettier: specifier: ^3.7.4 version: 3.8.3 tsup: specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.9)(typescript@5.9.3) + version: 8.5.1(postcss@8.5.15)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: specifier: ^8.50.1 version: 8.59.0(eslint@9.39.4)(typescript@5.9.3) + vite: + specifier: 8.0.16 + version: 8.0.16(@types/node@25.5.2)(esbuild@0.28.1) vitest: specifier: ^4.1.1 - version: 4.1.4(@types/node@25.5.2)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.27.7)) + version: 4.1.4(@types/node@25.5.2)(vite@8.0.16(@types/node@25.5.2)(esbuild@0.28.1)) packages: - '@emnapi/core@1.9.2': - resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@emnapi/runtime@1.9.2': - resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -262,7 +279,7 @@ packages: resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==} engines: {node: '>=18.14.1'} peerDependencies: - hono: '>=4.12.12' + hono: '>=4.12.25' '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} @@ -388,112 +405,112 @@ packages: resolution: {integrity: sha512-d0d4Oyxm+v980PEq1ZH2PmS6cvpMIRc17eYpiU47KgW+lzxklMu6+HOEOPmxrpnF/XQZ0+Q78I2mgMhbIIo/dg==} engines: {node: '>= 10'} - '@napi-rs/wasm-runtime@1.1.3': - resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@oxc-project/types@0.124.0': - resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} - '@rolldown/binding-android-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.15': - resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': - resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': - resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': - resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': - resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': - resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} - engines: {node: '>=14.0.0'} + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': - resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': - resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.15': - resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} '@rollup/rollup-android-arm-eabi@4.60.1': resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} @@ -636,8 +653,8 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -720,7 +737,7 @@ packages: resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==} peerDependencies: msw: ^2.4.9 - vite: '>=8.0.5' + vite: 8.0.16 peerDependenciesMeta: msw: optional: true @@ -806,7 +823,7 @@ packages: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} peerDependencies: - esbuild: '>=0.18' + esbuild: '>=0.28.1' bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} @@ -938,8 +955,8 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -1034,8 +1051,8 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -1116,10 +1133,13 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hono@4.12.12: - resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==} + hono@4.12.27: + resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==} engines: {node: '>=16.9.0'} + htm@3.1.1: + resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -1147,8 +1167,8 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ip-address@10.1.0: - resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -1333,8 +1353,8 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -1420,7 +1440,7 @@ packages: engines: {node: '>= 18'} peerDependencies: jiti: '>=1.21.0' - postcss: '>=8.0.9' + postcss: '>=8.5.10' tsx: ^4.8.1 yaml: ^2.4.2 peerDependenciesMeta: @@ -1433,10 +1453,13 @@ packages: yaml: optional: true - postcss@8.5.9: - resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + preact@10.29.2: + resolution: {integrity: sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -1454,8 +1477,8 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.15.1: - resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} range-parser@1.2.1: @@ -1482,8 +1505,8 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - rolldown@1.0.0-rc.15: - resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -1594,6 +1617,10 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} @@ -1625,7 +1652,7 @@ packages: peerDependencies: '@microsoft/api-extractor': ^7.36.0 '@swc/core': ^1 - postcss: ^8.4.12 + postcss: '>=8.5.10' typescript: '>=4.5.0' peerDependenciesMeta: '@microsoft/api-extractor': @@ -1674,14 +1701,14 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vite@8.0.8: - resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.0 - esbuild: ^0.27.0 || ^0.28.0 + '@vitejs/devtools': ^0.1.18 + esbuild: '>=0.28.1' jiti: '>=1.21.0' less: ^4.0.0 sass: ^1.70.0 @@ -1733,7 +1760,7 @@ packages: '@vitest/ui': 4.1.4 happy-dom: '*' jsdom: '*' - vite: '>=8.0.5' + vite: 8.0.16 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -1789,13 +1816,13 @@ packages: snapshots: - '@emnapi/core@1.9.2': + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.9.2': + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 optional: true @@ -1805,82 +1832,82 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.7': + '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.27.7': + '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.27.7': + '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.27.7': + '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.27.7': + '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.27.7': + '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.27.7': + '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.27.7': + '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.27.7': + '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.27.7': + '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.27.7': + '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.27.7': + '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.27.7': + '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.27.7': + '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.27.7': + '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.27.7': + '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.27.7': + '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.27.7': + '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.27.7': + '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.27.7': + '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.27.7': + '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.27.7': + '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.27.7': + '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.27.7': + '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.27.7': + '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.27.7': + '@esbuild/win32-x64@0.28.1': optional: true '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': @@ -1929,9 +1956,9 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 - '@hono/node-server@1.19.13(hono@4.12.12)': + '@hono/node-server@1.19.13(hono@4.12.27)': dependencies: - hono: 4.12.12 + hono: 4.12.27 '@humanfs/core@0.19.2': dependencies: @@ -1965,7 +1992,7 @@ snapshots: '@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)': dependencies: - '@hono/node-server': 1.19.13(hono@4.12.12) + '@hono/node-server': 1.19.13(hono@4.12.27) ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) content-type: 1.0.5 @@ -1975,7 +2002,7 @@ snapshots: eventsource-parser: 3.0.6 express: 5.2.1 express-rate-limit: 8.3.2(express@5.2.1) - hono: 4.12.12 + hono: 4.12.27 jose: 6.2.2 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -2036,65 +2063,65 @@ snapshots: '@napi-rs/keyring-win32-ia32-msvc': 1.2.0 '@napi-rs/keyring-win32-x64-msvc': 1.2.0 - '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 - '@tybys/wasm-util': 0.10.1 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 optional: true - '@oxc-project/types@0.124.0': {} + '@oxc-project/types@0.133.0': {} - '@rolldown/binding-android-arm64@1.0.0-rc.15': + '@rolldown/binding-android-arm64@1.0.3': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + '@rolldown/binding-darwin-arm64@1.0.3': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.15': + '@rolldown/binding-darwin-x64@1.0.3': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + '@rolldown/binding-freebsd-x64@1.0.3': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-arm64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + '@rolldown/binding-linux-arm64-musl@1.0.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-ppc64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-s390x-gnu@1.0.3': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-x64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + '@rolldown/binding-linux-x64-musl@1.0.3': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + '@rolldown/binding-openharmony-arm64@1.0.3': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + '@rolldown/binding-wasm32-wasi@1.0.3': dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 - '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + '@rolldown/binding-win32-arm64-msvc@1.0.3': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + '@rolldown/binding-win32-x64-msvc@1.0.3': optional: true - '@rolldown/pluginutils@1.0.0-rc.15': {} + '@rolldown/pluginutils@1.0.1': {} '@rollup/rollup-android-arm-eabi@4.60.1': optional: true @@ -2173,7 +2200,7 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@tybys/wasm-util@0.10.1': + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true @@ -2293,13 +2320,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.5.2)(esbuild@0.27.7))': + '@vitest/mocker@4.1.4(vite@8.0.16(@types/node@25.5.2)(esbuild@0.28.1))': dependencies: '@vitest/spy': 4.1.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.8(@types/node@25.5.2)(esbuild@0.27.7) + vite: 8.0.16(@types/node@25.5.2)(esbuild@0.28.1) '@vitest/pretty-format@4.1.4': dependencies: @@ -2350,7 +2377,7 @@ snapshots: ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 + fast-uri: 3.1.2 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -2376,7 +2403,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 - qs: 6.15.1 + qs: 6.15.2 raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: @@ -2391,9 +2418,9 @@ snapshots: dependencies: balanced-match: 4.0.4 - bundle-require@5.1.0(esbuild@0.27.7): + bundle-require@5.1.0(esbuild@0.28.1): dependencies: - esbuild: 0.27.7 + esbuild: 0.28.1 load-tsconfig: 0.2.5 bytes@3.1.2: {} @@ -2490,34 +2517,34 @@ snapshots: dependencies: es-errors: 1.3.0 - esbuild@0.27.7: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 + '@esbuild/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 escape-html@1.0.3: {} @@ -2608,7 +2635,7 @@ snapshots: express-rate-limit@8.3.2(express@5.2.1): dependencies: express: 5.2.1 - ip-address: 10.1.0 + ip-address: 10.2.0 express@5.2.1: dependencies: @@ -2632,7 +2659,7 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.15.1 + qs: 6.15.2 range-parser: 1.2.1 router: 2.2.0 send: 1.2.1 @@ -2649,7 +2676,7 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-uri@3.1.0: {} + fast-uri@3.1.2: {} fdir@6.5.0(picomatch@4.0.4): optionalDependencies: @@ -2731,7 +2758,9 @@ snapshots: dependencies: function-bind: 1.1.2 - hono@4.12.12: {} + hono@4.12.27: {} + + htm@3.1.1: {} http-errors@2.0.1: dependencies: @@ -2758,7 +2787,7 @@ snapshots: inherits@2.0.4: {} - ip-address@10.1.0: {} + ip-address@10.2.0: {} ipaddr.js@1.9.1: {} @@ -2899,7 +2928,7 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.11: {} + nanoid@3.3.15: {} natural-compare@1.4.0: {} @@ -2964,18 +2993,20 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 - postcss-load-config@6.0.1(postcss@8.5.9): + postcss-load-config@6.0.1(postcss@8.5.15): dependencies: lilconfig: 3.1.3 optionalDependencies: - postcss: 8.5.9 + postcss: 8.5.15 - postcss@8.5.9: + postcss@8.5.15: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 + preact@10.29.2: {} + prelude-ls@1.2.1: {} prettier@3.8.3: {} @@ -2987,7 +3018,7 @@ snapshots: punycode@2.3.1: {} - qs@6.15.1: + qs@6.15.2: dependencies: side-channel: 1.1.0 @@ -3008,26 +3039,26 @@ snapshots: resolve-from@5.0.0: {} - rolldown@1.0.0-rc.15: + rolldown@1.0.3: dependencies: - '@oxc-project/types': 0.124.0 - '@rolldown/pluginutils': 1.0.0-rc.15 + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.15 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 - '@rolldown/binding-darwin-x64': 1.0.0-rc.15 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 rollup@4.60.1: dependencies: @@ -3182,6 +3213,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tinyrainbow@3.1.0: {} toidentifier@1.0.1: {} @@ -3197,18 +3233,18 @@ snapshots: tslib@2.8.1: optional: true - tsup@8.5.1(postcss@8.5.9)(typescript@5.9.3): + tsup@8.5.1(postcss@8.5.15)(typescript@5.9.3): dependencies: - bundle-require: 5.1.0(esbuild@0.27.7) + bundle-require: 5.1.0(esbuild@0.28.1) cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 debug: 4.4.3 - esbuild: 0.27.7 + esbuild: 0.28.1 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.9) + postcss-load-config: 6.0.1(postcss@8.5.15) resolve-from: 5.0.0 rollup: 4.60.1 source-map: 0.7.6 @@ -3217,7 +3253,7 @@ snapshots: tinyglobby: 0.2.16 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.5.9 + postcss: 8.5.15 typescript: 5.9.3 transitivePeerDependencies: - jiti @@ -3260,22 +3296,22 @@ snapshots: vary@1.1.2: {} - vite@8.0.8(@types/node@25.5.2)(esbuild@0.27.7): + vite@8.0.16(@types/node@25.5.2)(esbuild@0.28.1): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.9 - rolldown: 1.0.0-rc.15 - tinyglobby: 0.2.16 + postcss: 8.5.15 + rolldown: 1.0.3 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 25.5.2 - esbuild: 0.27.7 + esbuild: 0.28.1 fsevents: 2.3.3 - vitest@4.1.4(@types/node@25.5.2)(vite@8.0.8(@types/node@25.5.2)(esbuild@0.27.7)): + vitest@4.1.4(@types/node@25.5.2)(vite@8.0.16(@types/node@25.5.2)(esbuild@0.28.1)): dependencies: '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.5.2)(esbuild@0.27.7)) + '@vitest/mocker': 4.1.4(vite@8.0.16(@types/node@25.5.2)(esbuild@0.28.1)) '@vitest/pretty-format': 4.1.4 '@vitest/runner': 4.1.4 '@vitest/snapshot': 4.1.4 @@ -3292,7 +3328,7 @@ snapshots: tinyexec: 1.1.1 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.8(@types/node@25.5.2)(esbuild@0.27.7) + vite: 8.0.16(@types/node@25.5.2)(esbuild@0.28.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.5.2 diff --git a/scripts/build-dashboard-client.mjs b/scripts/build-dashboard-client.mjs new file mode 100644 index 0000000..cb96300 --- /dev/null +++ b/scripts/build-dashboard-client.mjs @@ -0,0 +1,53 @@ +#!/usr/bin/env node +/** + * Bundles the status-dashboard browser client (Preact + htm + the app) into a + * single minified IIFE and writes it to `src/core/dashboard-client.ts` as a + * string constant. + * + * Why a generated string instead of a CDN or runtime file read: + * the dashboard is served as one self-contained, offline, CSP-clean HTML page, + * and the published package ships only `dist/`. Bundling the client here also + * sidesteps a template-literal collision — htm uses `html`...${x}`` syntax, + * which can't live inside the `getDashboardHtml()` TS template literal. + * + * The generated file is committed so `pnpm run build` works without a separate + * bundling step in consumers; it is regenerated automatically by `build`. + */ +import { build } from "esbuild"; +import { readFileSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = join(here, ".."); +const entry = join(root, "src", "dashboard-client", "main.js"); +const outPath = join(root, "src", "core", "dashboard-client.ts"); +const nm = join(root, "node_modules"); + +const result = await build({ + entryPoints: [entry], + bundle: true, + minify: true, + format: "iife", + platform: "browser", + target: ["es2019"], + write: false, + legalComments: "none", +}); + +const code = result.outputFiles[0].text.trim(); + +const htmPkg = JSON.parse(readFileSync(join(nm, "htm", "package.json"), "utf8")); +const preactPkg = JSON.parse(readFileSync(join(nm, "preact", "package.json"), "utf8")); + +const banner = + `// AUTO-GENERATED by scripts/build-dashboard-client.mjs — do not edit by hand.\n` + + `// Bundled status-dashboard client: Preact ${preactPkg.version} + htm ${htmPkg.version} + app.\n` + + `// Source: src/dashboard-client/main.js. Regenerate with: pnpm run build:dashboard\n\n`; + +const body = `export const DASHBOARD_CLIENT = ${JSON.stringify(code)};\n`; + +writeFileSync(outPath, banner + body, "utf8"); +console.log( + `Wrote ${outPath} (${(code.length / 1024).toFixed(1)} KB minified, preact@${preactPkg.version} htm@${htmPkg.version})`, +); diff --git a/src/__tests__/core/import.test.ts b/src/__tests__/core/import.test.ts index 93ee9a3..bbe75be 100644 --- a/src/__tests__/core/import.test.ts +++ b/src/__tests__/core/import.test.ts @@ -50,4 +50,11 @@ describe("parseDotenv", () => { const result = parseDotenv('TAG="v1.0#beta"'); expect(result.get("TAG")).toBe("v1.0#beta"); }); + + it("preserves a hash with no preceding space in unquoted values", () => { + const result = parseDotenv("COLOR=#ff0000"); + expect(result.get("COLOR")).toBe("#ff0000"); + const r2 = parseDotenv("FRAGMENT=foo#bar"); + expect(r2.get("FRAGMENT")).toBe("foo#bar"); + }); }); diff --git a/src/__tests__/core/noise.test.ts b/src/__tests__/core/noise.test.ts index fee7890..2c92033 100644 --- a/src/__tests__/core/noise.test.ts +++ b/src/__tests__/core/noise.test.ts @@ -40,6 +40,21 @@ describe("generateSecret", () => { expect(s.length).toBe(24); }); + it("guarantees every character class even at short lengths, across many runs", () => { + for (let i = 0; i < 500; i++) { + const pw = generateSecret({ format: "password", length: 8 }); + expect(pw).toHaveLength(8); + expect(/[A-Z]/.test(pw)).toBe(true); + expect(/[a-z]/.test(pw)).toBe(true); + expect(/[0-9]/.test(pw)).toBe(true); + expect(/[^A-Za-z0-9]/.test(pw)).toBe(true); + } + }); + + it("does not throw for very short password lengths", () => { + expect(() => generateSecret({ format: "password", length: 2 })).not.toThrow(); + }); + it("produces unique values on successive calls", () => { const a = generateSecret(); const b = generateSecret(); diff --git a/src/__tests__/core/policy-root.test.ts b/src/__tests__/core/policy-root.test.ts new file mode 100644 index 0000000..4b4866a --- /dev/null +++ b/src/__tests__/core/policy-root.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { checkToolPolicy, checkKeyReadPolicy, setPolicyRoot, clearPolicyCache } from "../../core/policy.js"; +import { mkdirSync, writeFileSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +/** + * Isolated file: setPolicyRoot mutates module-global state, and vitest isolates + * modules per file, so this won't leak into the other policy test suites. + */ +describe("setPolicyRoot anchors governance to a trusted root", () => { + const rootDir = join(tmpdir(), `qring-policy-root-${Date.now()}`); + const otherDir = join(tmpdir(), `qring-policy-other-${Date.now()}`); + + beforeAll(() => { + mkdirSync(rootDir, { recursive: true }); + mkdirSync(otherDir, { recursive: true }); + // Trusted root denies a tool/key; the "other" dir has no policy at all. + writeFileSync( + join(rootDir, ".q-ring.json"), + JSON.stringify({ + policy: { mcp: { denyTools: ["get_secret"], deniedKeys: ["PROD_DB"] } }, + }), + "utf8", + ); + setPolicyRoot(rootDir); + clearPolicyCache(); + }); + + afterAll(() => { + rmSync(rootDir, { recursive: true, force: true }); + rmSync(otherDir, { recursive: true, force: true }); + }); + + it("ignores an agent-supplied projectPath that points away from the root", () => { + // Even though the caller points at a dir with no policy, the pinned root + // still applies — the agent cannot escape governance. + expect(checkToolPolicy("get_secret", otherDir).allowed).toBe(false); + expect(checkKeyReadPolicy("PROD_DB", undefined, otherDir).allowed).toBe(false); + }); + + it("still enforces the root policy when no projectPath is given", () => { + expect(checkToolPolicy("get_secret").allowed).toBe(false); + }); +}); diff --git a/src/__tests__/core/policy.test.ts b/src/__tests__/core/policy.test.ts index b8c5ede..ecedc74 100644 --- a/src/__tests__/core/policy.test.ts +++ b/src/__tests__/core/policy.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { checkToolPolicy, checkExecPolicy, @@ -6,6 +6,9 @@ import { getPolicySummary, clearPolicyCache, } from "../../core/policy.js"; +import { mkdirSync, writeFileSync, rmSync, utimesSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; describe("policy (no .q-ring.json)", () => { beforeEach(() => { @@ -34,3 +37,43 @@ describe("policy (no .q-ring.json)", () => { expect(summary.hasSecretPolicy).toBe(false); }); }); + +describe("checkExecPolicy (with .q-ring.json)", () => { + const dir = join(tmpdir(), `qring-policy-exec-${Date.now()}`); + + beforeEach(() => { + clearPolicyCache(); + mkdirSync(dir, { recursive: true }); + }); + afterEach(() => { + clearPolicyCache(); + rmSync(dir, { recursive: true, force: true }); + }); + + it("denies on token/path boundaries, not bare substrings", () => { + writeFileSync( + join(dir, ".q-ring.json"), + JSON.stringify({ policy: { exec: { denyCommands: ["rm"] } } }), + "utf8", + ); + clearPolicyCache(); + // bare substring must NOT trip the deny + expect(checkExecPolicy("charm build", dir).allowed).toBe(true); + // real invocations (with path or args) must be denied + expect(checkExecPolicy("rm -rf build", dir).allowed).toBe(false); + expect(checkExecPolicy("/usr/bin/rm file", dir).allowed).toBe(false); + }); + + it("reloads policy when .q-ring.json changes (mtime invalidation)", () => { + const file = join(dir, ".q-ring.json"); + writeFileSync(file, JSON.stringify({ policy: { exec: { denyCommands: ["curl"] } } }), "utf8"); + clearPolicyCache(); + expect(checkExecPolicy("curl example.com", dir).allowed).toBe(false); + + // Rewrite without clearing the cache; bump mtime so invalidation triggers. + writeFileSync(file, JSON.stringify({ policy: { exec: {} } }), "utf8"); + const future = new Date(Date.now() + 5000); + utimesSync(file, future, future); + expect(checkExecPolicy("curl example.com", dir).allowed).toBe(true); + }); +}); diff --git a/src/__tests__/core/ssrf.test.ts b/src/__tests__/core/ssrf.test.ts index 8507203..94c60d9 100644 --- a/src/__tests__/core/ssrf.test.ts +++ b/src/__tests__/core/ssrf.test.ts @@ -1,5 +1,11 @@ -import { describe, it, expect } from "vitest"; -import { isPrivateIP, checkSSRF, checkSSRFSync } from "../../core/ssrf.js"; +import { describe, it, expect, afterEach } from "vitest"; +import { isPrivateIP, checkSSRF, checkSSRFSync, guardedLookup } from "../../core/ssrf.js"; + +function lookupVia(host: string): Promise<{ err: NodeJS.ErrnoException | null }> { + return new Promise((resolve) => { + guardedLookup(host, {}, (err) => resolve({ err })); + }); +} describe("isPrivateIP", () => { it("blocks 127.x.x.x (loopback)", () => { @@ -95,3 +101,30 @@ describe("checkSSRFSync", () => { expect(checkSSRFSync("https://example.com/path")).toBeNull(); }); }); + +describe("guardedLookup (connect-time DNS-rebinding guard)", () => { + const orig = process.env.Q_RING_ALLOW_PRIVATE_HOOKS; + afterEach(() => { + if (orig === undefined) delete process.env.Q_RING_ALLOW_PRIVATE_HOOKS; + else process.env.Q_RING_ALLOW_PRIVATE_HOOKS = orig; + }); + + it("blocks resolution to a private/loopback address", async () => { + delete process.env.Q_RING_ALLOW_PRIVATE_HOOKS; + const { err } = await lookupVia("127.0.0.1"); + expect(err).not.toBeNull(); + expect(err?.code).toBe("EQRINGSSRF"); + }); + + it("allows resolution to a public address", async () => { + delete process.env.Q_RING_ALLOW_PRIVATE_HOOKS; + const { err } = await lookupVia("8.8.8.8"); + expect(err).toBeNull(); + }); + + it("respects the Q_RING_ALLOW_PRIVATE_HOOKS override", async () => { + process.env.Q_RING_ALLOW_PRIVATE_HOOKS = "1"; + const { err } = await lookupVia("127.0.0.1"); + expect(err).toBeNull(); + }); +}); diff --git a/src/__tests__/core/teleport.test.ts b/src/__tests__/core/teleport.test.ts index 018b2f6..5a80777 100644 --- a/src/__tests__/core/teleport.test.ts +++ b/src/__tests__/core/teleport.test.ts @@ -1,6 +1,34 @@ import { describe, it, expect } from "vitest"; +import { createCipheriv, pbkdf2Sync, randomBytes } from "node:crypto"; import { teleportPack, teleportUnpack } from "../../core/teleport.js"; +/** Build a pre-bump bundle (PBKDF2 100k, no `iter` field) the old code produced. */ +function legacyBundle( + secrets: { key: string; value: string; scope?: string }[], + passphrase: string, +): string { + const plaintext = JSON.stringify({ + secrets, + exportedAt: new Date().toISOString(), + }); + const salt = randomBytes(32); + const iv = randomBytes(12); + const key = pbkdf2Sync(passphrase, salt, 100000, 32, "sha512"); + const cipher = createCipheriv("aes-256-gcm", key, iv); + const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); + const tag = cipher.getAuthTag(); + const bundle = { + v: 1, + data: encrypted.toString("base64"), + salt: salt.toString("base64"), + iv: iv.toString("base64"), + tag: tag.toString("base64"), + createdAt: new Date().toISOString(), + count: secrets.length, + }; + return Buffer.from(JSON.stringify(bundle)).toString("base64"); +} + describe("teleport pack/unpack", () => { const secrets = [ { key: "API_KEY", value: "sk-abc123", scope: "project" }, @@ -56,4 +84,19 @@ describe("teleport pack/unpack", () => { const payload = teleportUnpack(bundle, passphrase); expect(payload.secrets).toHaveLength(0); }); + + it("records the PBKDF2 iteration count in new bundles", () => { + const bundle = teleportPack(secrets, passphrase); + const inner = JSON.parse( + Buffer.from(bundle, "base64").toString("utf8"), + ) as { iter?: number }; + expect(inner.iter).toBe(210000); + }); + + it("still decrypts legacy bundles that predate the iteration bump", () => { + const bundle = legacyBundle(secrets, passphrase); + const payload = teleportUnpack(bundle, passphrase); + expect(payload.secrets[0].value).toBe("sk-abc123"); + expect(payload.secrets[1].value).toBe("p@ssw0rd"); + }); }); diff --git a/src/cli/commands/tooling.ts b/src/cli/commands/tooling.ts index 5ad3dc7..4a76d63 100644 --- a/src/cli/commands/tooling.ts +++ b/src/cli/commands/tooling.ts @@ -168,8 +168,7 @@ export function registerToolingCommands(program: Command): void { const { platform } = await import("node:os"); const port = Number(cmd.port); - const { close } = startDashboardServer({ port }); - const url = `http://127.0.0.1:${port}`; + const { close, url } = startDashboardServer({ port }); console.log( `\n ${SYMBOLS.zap} ${c.bold("q-ring quantum status dashboard")}\n`, diff --git a/src/core/dashboard-client.ts b/src/core/dashboard-client.ts new file mode 100644 index 0000000..278a87a --- /dev/null +++ b/src/core/dashboard-client.ts @@ -0,0 +1,5 @@ +// AUTO-GENERATED by scripts/build-dashboard-client.mjs — do not edit by hand. +// Bundled status-dashboard client: Preact 10.29.2 + htm 3.1.1 + app. +// Source: src/dashboard-client/main.js. Regenerate with: pnpm run build:dashboard + +export const DASHBOARD_CLIENT = "\"use strict\";(()=>{var G,g,we,et,N,ge,Ce,Se,se,W,L,Me,le,ae,oe,tt,O={},Y=[],nt=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Q=Array.isArray;function R(t,e){for(var n in e)t[n]=e[n];return t}function re(t){t&&t.parentNode&&t.parentNode.removeChild(t)}function ce(t,e,n){var s,o,a,l={};for(a in e)a==\"key\"?s=e[a]:a==\"ref\"?o=e[a]:l[a]=e[a];if(arguments.length>2&&(l.children=arguments.length>3?G.call(arguments,2):n),typeof t==\"function\"&&t.defaultProps!=null)for(a in t.defaultProps)l[a]===void 0&&(l[a]=t.defaultProps[a]);return z(t,l,s,o,null)}function z(t,e,n,s,o){var a={type:t,props:e,key:n,ref:s,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o==null?++we:o,__i:-1,__u:0};return o==null&&g.vnode!=null&&g.vnode(a),a}function X(t){return t.children}function K(t,e){this.props=t,this.context=e}function D(t,e){if(e==null)return t.__?D(t.__,t.__i+1):null;for(var n;ee&&N.sort(Se),t=N.shift(),e=N.length,st(t)}finally{N.length=J.__r=0}}function Ee(t,e,n,s,o,a,l,d,_,c,i){var r,p,v,f,C,k,y,h=s&&s.__k||Y,$=e.length;for(_=at(n,e,h,_,$),r=0;r<$;r++)(v=n.__k[r])!=null&&(p=v.__i!=-1&&h[v.__i]||O,v.__i=r,k=de(t,v,p,o,a,l,d,_,c,i),f=v.__e,v.ref&&p.ref!=v.ref&&(p.ref&&ue(p.ref,null,v),i.push(v.ref,v.__c||f,v)),C==null&&f!=null&&(C=f),(y=!!(4&v.__u))||p.__k===v.__k?(_=Pe(v,_,t,y),y&&p.__e&&(p.__e=null)):typeof v.type==\"function\"&&k!==void 0?_=k:f&&(_=f.nextSibling),v.__u&=-7);return n.__e=C,_}function at(t,e,n,s,o){var a,l,d,_,c,i=n.length,r=i,p=0;for(t.__k=new Array(o),a=0;a0?l=t.__k[a]=z(l.type,l.props,l.key,l.ref?l.ref:null,l.__v):t.__k[a]=l,_=a+p,l.__=t,l.__b=t.__b+1,d=null,(c=l.__i=ot(l,n,_,r))!=-1&&(r--,(d=n[c])&&(d.__u|=2)),d==null||d.__v==null?(c==-1&&(o>i?p--:o_?p--:p++,l.__u|=4))):t.__k[a]=null;if(r)for(a=0;a(i?1:0)){for(o=n-1,a=n+1;o>=0||a=0?o--:a++])!=null&&(2&c.__u)==0&&d==c.key&&_==c.type)return l}return-1}function be(t,e,n){e[0]==\"-\"?t.setProperty(e,n==null?\"\":n):t[e]=n==null?\"\":typeof n!=\"number\"||nt.test(e)?n:n+\"px\"}function V(t,e,n,s,o){var a,l;e:if(e==\"style\")if(typeof n==\"string\")t.style.cssText=n;else{if(typeof s==\"string\"&&(t.style.cssText=s=\"\"),s)for(e in s)n&&e in n||be(t.style,e,\"\");if(n)for(e in n)s&&n[e]==s[e]||be(t.style,e,n[e])}else if(e[0]==\"o\"&&e[1]==\"n\")a=e!=(e=e.replace(Me,\"$1\")),l=e.toLowerCase(),e=l in t||e==\"onFocusOut\"||e==\"onFocusIn\"?l.slice(2):e.slice(2),t.l||(t.l={}),t.l[e+a]=n,n?s?n[L]=s[L]:(n[L]=le,t.addEventListener(e,a?oe:ae,a)):t.removeEventListener(e,a?oe:ae,a);else{if(o==\"http://www.w3.org/2000/svg\")e=e.replace(/xlink(H|:h)/,\"h\").replace(/sName$/,\"s\");else if(e!=\"width\"&&e!=\"height\"&&e!=\"href\"&&e!=\"list\"&&e!=\"form\"&&e!=\"tabIndex\"&&e!=\"download\"&&e!=\"rowSpan\"&&e!=\"colSpan\"&&e!=\"role\"&&e!=\"popover\"&&e in t)try{t[e]=n==null?\"\":n;break e}catch{}typeof n==\"function\"||(n==null||n===!1&&e[4]!=\"-\"?t.removeAttribute(e):t.setAttribute(e,e==\"popover\"&&n==1?\"\":n))}}function xe(t){return function(e){if(this.l){var n=this.l[e.type+t];if(e[W]==null)e[W]=le++;else if(e[W]0?t:Q(t)?t.map(Te):t.constructor!==void 0?null:R({},t)}function it(t,e,n,s,o,a,l,d,_){var c,i,r,p,v,f,C,k=n.props||O,y=e.props,h=e.type;if(h==\"svg\"?o=\"http://www.w3.org/2000/svg\":h==\"math\"?o=\"http://www.w3.org/1998/Math/MathML\":o||(o=\"http://www.w3.org/1999/xhtml\"),a!=null){for(c=0;c=n.__.length&&n.__.push({}),n.__[t]}function q(t){return I=1,rt(ze,t)}function rt(t,e,n){var s=ve(U++,2);if(s.t=t,!s.__c&&(s.__=[n?n(e):ze(void 0,e),function(d){var _=s.__N?s.__N[0]:s.__[0],c=s.t(_,d);_!==c&&(s.__N=[c,s.__[1]],s.__c.setState({}))}],s.__c=x,!x.__f)){var o=function(d,_,c){if(!s.__c.__H)return!0;var i=s.__c.__H.__.filter(function(p){return p.__c});if(i.every(function(p){return!p.__N}))return!a||a.call(this,d,_,c);var r=s.__c.props!==d;return i.some(function(p){if(p.__N){var v=p.__[0];p.__=p.__N,p.__N=void 0,v!==p.__[0]&&(r=!0)}}),a&&a.call(this,d,_,c)||r};x.__f=!0;var a=x.shouldComponentUpdate,l=x.componentWillUpdate;x.componentWillUpdate=function(d,_,c){if(this.__e){var i=a;a=void 0,o(d,_,c),a=i}l&&l.call(this,d,_,c)},x.shouldComponentUpdate=o}return s.__N||s.__}function ee(t,e){var n=ve(U++,3);!w.__s&&We(n.__H,e)&&(n.__=t,n.u=e,x.__H.__h.push(n))}function fe(t){return I=5,te(function(){return{current:t}},[])}function te(t,e){var n=ve(U++,7);return We(n.__H,e)&&(n.__=t(),n.__H=e,n.__h=t),n.__}function ye(t,e){return I=8,te(function(){return t},e)}function ct(){for(var t;t=Ve.shift();){var e=t.__H;if(t.__P&&e)try{e.__h.some(Z),e.__h.some(pe),e.__h=[]}catch(n){e.__h=[],w.__e(n,t.__v)}}}w.__b=function(t){x=null,De&&De(t)},w.__=function(t,e){t&&e.__k&&e.__k.__m&&(t.__m=e.__k.__m),Fe&&Fe(t,e)},w.__r=function(t){je&&je(t),U=0;var e=(x=t.__c).__H;e&&(_e===x?(e.__h=[],x.__h=[],e.__.some(function(n){n.__N&&(n.__=n.__N),n.u=n.__N=void 0})):(e.__h.some(Z),e.__h.some(pe),e.__h=[],U=0)),_e=x},w.diffed=function(t){Le&&Le(t);var e=t.__c;e&&e.__H&&(e.__H.__h.length&&(Ve.push(e)!==1&&He===w.requestAnimationFrame||((He=w.requestAnimationFrame)||dt)(ct)),e.__H.__.some(function(n){n.u&&(n.__H=n.u),n.u=void 0})),_e=x=null},w.__c=function(t,e){e.some(function(n){try{n.__h.some(Z),n.__h=n.__h.filter(function(s){return!s.__||pe(s)})}catch(s){e.some(function(o){o.__h&&(o.__h=[])}),e=[],w.__e(s,n.__v)}}),Ue&&Ue(t,e)},w.unmount=function(t){Ie&&Ie(t);var e,n=t.__c;n&&n.__H&&(n.__H.__.some(function(s){try{Z(s)}catch(o){e=o}}),n.__H=void 0,e&&w.__e(e,n.__v))};var Be=typeof requestAnimationFrame==\"function\";function dt(t){var e,n=function(){clearTimeout(s),Be&&cancelAnimationFrame(e),setTimeout(t)},s=setTimeout(n,35);Be&&(e=requestAnimationFrame(n))}function Z(t){var e=x,n=t.__c;typeof n==\"function\"&&(t.__c=void 0,n()),x=e}function pe(t){var e=x;t.__c=t.__(),x=e}function We(t,e){return!t||t.length!==e.length||e.some(function(n,s){return n!==t[s]})}function ze(t,e){return typeof e==\"function\"?e(t):e}var Oe=function(t,e,n,s){var o;e[0]=0;for(var a=1;a=5&&((l||!p&&a===5)&&(_.push(a,0,l,o),a=6),p&&(_.push(a,p,0,o),a=6)),l=\"\"},i=0;i\"?(a=1,l=\"\"):l=s+l[0]:d?s===d?d=\"\":l+=s:s==='\"'||s===\"'\"?d=s:s===\">\"?(c(),a=1):a&&(s===\"=\"?(a=5,o=l,l=\"\"):s===\"/\"&&(a<5||n[i][r+1]===\">\")?(c(),a===3&&(_=_[0]),a=_,(_=_[0]).push(2,0,a),a=0):s===\" \"||s===\"\t\"||s===`\n`||s===\"\\r\"?(c(),a=2):l+=s),a===3&&l===\"!--\"&&(a=4,_=_[0])}return c(),_})(t)),e),arguments,[])).length>1?e:e[0]}var u=Ye.bind(ce),Je=window.__QRING__&&window.__QRING__.token||\"\",he=Je?\"?token=\"+encodeURIComponent(Je):\"\",A=t=>''+t+\"\",m={health:A(''),environment:A(''),decay:A(''),superposition:A(''),entangle:A(''),tunnel:A(''),anomaly:A(''),audit:A(''),key:A(''),hook:A(''),approve:A(''),manifest:A(''),policy:A(''),memory:A(''),folder:'',search:'',lock:''};function me(t){if(!t)return\"env-default\";let e=t.toLowerCase();return e===\"prod\"||e===\"production\"?\"env-prod\":e===\"staging\"||e===\"stage\"?\"env-staging\":e===\"dev\"||e===\"development\"?\"env-dev\":e===\"test\"||e===\"testing\"?\"env-test\":\"env-default\"}function Qe(t,e){return e||t>=90?\"var(--danger)\":t>=75?\"var(--warning)\":\"var(--accent)\"}function Xe(t){let e=new Date(t);return String(e.getHours()).padStart(2,\"0\")+\":\"+String(e.getMinutes()).padStart(2,\"0\")+\":\"+String(e.getSeconds()).padStart(2,\"0\")}function Ze(t){if(!t)return\"\\u2014\";let e=Math.floor((Date.now()-new Date(t).getTime())/1e3);return isNaN(e)?\"\\u2014\":e<5?\"just now\":e<60?e+\"s ago\":e<3600?Math.floor(e/60)+\"m ago\":e<86400?Math.floor(e/3600)+\"h ago\":Math.floor(e/86400)+\"d ago\"}function $e(t){return t==null?\"\\u2014\":t<0?\"expired\":t<60?t+\"s\":t<3600?Math.floor(t/60)+\"m\":t<86400?Math.floor(t/3600)+\"h\":Math.floor(t/86400)+\"d\"}var j=({svg:t})=>u``;function S({icon:t,title:e,aside:n,asideStyle:s}){return u`
\n <${j} svg=${t} />${e}\n ${n!=null?u`${n}`:null}\n
`}function M({delay:t=0,wide:e=!1,style:n=\"\",children:s}){let o=\"card\"+(e?\" grid-wide\":\"\"),a=\"animation-delay:\"+t+\"ms\"+(n?\";\"+n:\"\");return u`
${s}
`}function E({children:t,cta:e}){return u`
${t}${e?u`${e}`:null}
`}function Ge({snap:t,paused:e,connState:n,onPause:s,onRefresh:o,lastUpdate:a}){let l=e?\"paused\":n===\"live\"?\"live\":n===\"reconnecting\"?\"reconnecting\\u2026\":\"connecting\\u2026\",d=\"status-dot\"+(e?\" paused\":n===\"reconnecting\"?\" disconnected\":\"\");return u`\n
\n
\n

\n \n q-ring\n quantum status\n

\n \n \n ${\" v\"+(t?t.version:\"\")}\n \n \n <${j} svg=${m.folder} /> ${t?t.projectPath:\"\"}\n \n
\n
\n ${a}\n \n \n \n \n JSON\n \n ${l}\n
\n
\n `}function ut({snap:t}){let e=t,n=e.auditMetrics||{total:0,byAction:{},topRead:[]},s=n.byAction.read||0,o=(n.byAction.write||0)+(n.byAction.delete||0)+(n.byAction.rotate||0),a=n.byAction.policy_deny||0,l=(e.hooks||[]).filter(i=>i.enabled).length,d=(e.approvals||[]).filter(i=>i.valid).length,_=(e.anomalies||[]).length,c=[{label:\"Secrets\",icon:m.key,value:e.health.total,sub:e.health.healthy+\" healthy \\xB7 \"+e.health.stale+\" stale \\xB7 \"+e.health.expired+\" expired\",cls:\"\"},{label:\"Environment\",icon:m.environment,value:e.environment?e.environment.env:\"none\",sub:e.environment?\"via \"+e.environment.source:\"no env detected\",cls:e.environment?\"\":\"dim\"},{label:\"Protected\",icon:m.lock,value:e.protectedCount,sub:\"require approval\",cls:e.protectedCount>0?\"warning\":\"dim\"},{label:\"Approvals\",icon:m.approve,value:d,sub:\"active grants\",cls:d>0?\"green\":\"dim\"},{label:\"Hooks\",icon:m.hook,value:l,sub:\"enabled / \"+(e.hooks||[]).length+\" total\",cls:l>0?\"\":\"dim\"},{label:\"Reads (24h)\",icon:m.audit,value:s,sub:o+\" writes \\xB7 \"+a+\" denied\",cls:\"\"},{label:\"Anomalies\",icon:m.anomaly,value:_,sub:_?\"investigate now\":\"all clear\",cls:_?\"danger\":\"green\"}];return u`
\n ${c.map(i=>u`
\n
<${j} svg=${i.icon} />${i.label}
\n
${i.value}
\n
${i.sub}
\n
`)}\n
`}function _t({snap:t,delay:e}){let n=t.health,s=n.total||1,o=42,a=2*Math.PI*o,l=[{v:n.healthy,c:\"var(--accent)\"},{v:n.stale,c:\"var(--warning)\"},{v:n.expired,c:\"var(--danger)\"},{v:n.noDecay,c:\"var(--text-dim)\"}],d=0,_=l.map((i,r)=>{let p=i.v/s*a,v=u``;return d+=p,v}),c=t.scopes||{global:0,project:0,team:0,org:0};return u`<${M} delay=${e}>\n <${S} icon=${m.health} title=\"Health Summary\" aside=${n.healthy+\"/\"+n.total+\" healthy\"} />\n
\n
\n ${_}\n
${n.total}secrets
\n
\n
\n
Healthy ${n.healthy}
\n
Stale ${n.stale}
\n
Expired ${n.expired}
\n
No decay ${n.noDecay}
\n
\n
\n
\n ${[\"global\",\"project\",\"team\",\"org\"].filter(i=>c[i]).map(i=>u`${c[i]}${i}`)}\n
\n `}function pt({snap:t,delay:e}){let n=t.environment;return u`<${M} delay=${e}>\n <${S} icon=${m.environment} title=\"Environment\" />\n ${n?u`
${n.env}detected via ${n.source}
`:u`<${E} cta=\"$ qring env --project-path .\">No environment detected.`}\n `}function vt({snap:t,delay:e}){let n=t.manifest;if(!n)return u`<${M} delay=${e}>\n <${S} icon=${m.manifest} title=\"Manifest\" />\n <${E} cta=${u`$ qring wizard `}>No .q-ring.json manifest in this project.\n `;let s=n.required-n.missing.length-n.expired.length-n.stale.length,o=n.required?Math.max(0,Math.round(s/n.required*100)):100,a=n.missing.length?\"var(--danger)\":n.expired.length||n.stale.length?\"var(--warning)\":\"var(--green)\",l=(_,c,i)=>u`
${_} (${c.length})
${c.map(r=>u`${r}`)}
`,d=n.missing.length||n.expired.length||n.stale.length;return u`<${M} delay=${e}>\n <${S} icon=${m.manifest} title=\"Manifest\" aside=${n.required+\" required / \"+n.declared+\" declared\"} />\n
\n
healthy
${o}%
\n
\n
\n ${n.missing.length?l(\"Missing required\",n.missing,\"miss\"):null}\n ${n.expired.length?l(\"Expired\",n.expired,\"exp\"):null}\n ${n.stale.length?l(\"Stale\",n.stale,\"exp\"):null}\n ${d?null:u`
All required keys present and healthy.
`}\n
\n `}function ft({snap:t,delay:e}){let n=t.policy||{counts:{}},s=n.counts||{};if(!(n.hasMcpPolicy||n.hasExecPolicy||n.hasSecretPolicy))return u`<${M} delay=${e}>\n <${S} icon=${m.policy} title=\"Policy\" />\n <${E} cta=${u`add policy to .q-ring.json`}>No governance policy declared.\n `;let a=({label:d,value:_})=>u`
${d}${_||\"\\u2014\"}
`,l=[n.hasMcpPolicy?\"mcp\":\"\",n.hasExecPolicy?\"exec\":\"\",n.hasSecretPolicy?\"secrets\":\"\"].filter(Boolean).join(\" \\xB7 \")||\"none\";return u`<${M} delay=${e}>\n <${S} icon=${m.policy} title=\"Policy\" aside=${l} />\n
\n <${a} label=\"MCP allow tools\" value=${s.allowTools} />\n <${a} label=\"MCP deny tools\" value=${s.denyTools} />\n <${a} label=\"Denied keys\" value=${s.deniedKeys} />\n <${a} label=\"Denied tags\" value=${s.deniedTags} />\n <${a} label=\"Exec allow\" value=${s.allowCommands} />\n <${a} label=\"Exec deny\" value=${s.denyCommands} />\n <${a} label=\"Tag → approval\" value=${s.requireApprovalForTags} />\n <${a} label=\"Tag → rotation\" value=${s.requireRotationFormatForTags} />\n ${n.maxTtlSeconds?u`<${a} label=\"Max TTL\" value=${$e(n.maxTtlSeconds)} />`:null}\n ${n.maxRuntimeSeconds?u`<${a} label=\"Max exec runtime\" value=${$e(n.maxRuntimeSeconds)} />`:null}\n
\n `}function yt({snap:t,delay:e}){let n=(t.secrets||[]).filter(s=>s.decay&&s.decay.timeRemaining).sort((s,o)=>(s.decay.secondsRemaining||0)-(o.decay.secondsRemaining||0));return u`<${M} delay=${e}>\n <${S} icon=${m.decay} title=\"Decay Timers\" aside=${n.length+\" tracked\"} />\n ${n.length?u`
${n.slice(0,12).map(s=>{let o=Math.min(s.decay.lifetimePercent,100),a=Qe(o,s.decay.isExpired),l=s.decay.isExpired?\"expired\":s.decay.timeRemaining||\"\";return u`
\n ${s.key}\n
\n ${l}\n
`})}
`:u`<${E} cta=\"$ qring set KEY --ttl 86400\">No secrets with decay configured.`}\n `}function ht({snap:t,delay:e}){let n=(t.secrets||[]).filter(s=>s.type===\"superposition\"&&s.environments&&s.environments.length);return u`<${M} delay=${e}>\n <${S} icon=${m.superposition} title=\"Superposition\" aside=${n.length+\" multi-env\"} />\n ${n.length?u`
${n.slice(0,14).map(s=>u`
\n ${s.key}\n ${(s.environments||[]).map(o=>u`${o}${o===s.defaultEnv?\" \\u2713\":\"\"}`)}\n
`)}
`:u`<${E} cta=\"$ qring set KEY --env prod\">No secrets in superposition.`}\n `}function $t({snap:t,delay:e}){let n=t.entanglements||[],s=new Set,o=n.filter(a=>{let l=[a.source.service,a.source.key,a.target.service,a.target.key].sort().join(\"|\");return s.has(l)?!1:(s.add(l),!0)});return u`<${M} delay=${e}>\n <${S} icon=${m.entangle} title=\"Entanglement\" aside=${o.length+\" pair\"+(o.length===1?\"\":\"s\")} />\n ${o.length?u`
${o.slice(0,12).map((a,l)=>u`
\n ${a.source.key}${a.target.key}\n
`)}
`:u`<${E} cta=\"$ qring entangle KEY_A KEY_B\">No entangled secrets.`}\n `}function mt({snap:t,delay:e}){let n=t.tunnels||[];return u`<${M} delay=${e}>\n <${S} icon=${m.tunnel} title=\"Quantum Tunnels\" aside=${n.length+\" live\"} />\n ${n.length?u`
${n.map(s=>{let o=s.expiresAt?Math.max(0,Math.floor((s.expiresAt-Date.now())/1e3)):null;return u`
${s.id}
\n reads: ${s.accessCount}${s.maxReads?\"/\"+s.maxReads:\"\"}\n ${o!==null?u`expires: ${o}s`:u`no expiry`}\n
`})}
`:u`<${E} cta=${u`$ qring tunnel create `}>No active tunnels.`}\n `}function gt({snap:t,delay:e}){let n=t.approvals||[];return u`<${M} delay=${e}>\n <${S} icon=${m.approve} title=\"Approvals\" aside=${n.filter(s=>s.valid).length+\" valid\"} />\n ${n.length?u`
${n.slice(0,8).map(s=>{let o=s.tampered?\"tampered\":s.secondsRemaining<300?\"expiring\":\"\";return u`
\n
${s.key}${s.tampered?\" \\u26A0 TAMPERED\":\"\"}${$e(s.secondsRemaining)} left
\n
${s.reason||\"(no reason)\"}
\n
scope:${s.scope}by:${s.grantedBy}
\n
`})}
`:u`<${E} cta=${'$ qring approve KEY --for 1800 --reason \"...\"'}>No active approvals.`}\n `}function kt({snap:t,delay:e}){let n=t.hooks||[];return u`<${M} delay=${e}>\n <${S} icon=${m.hook} title=\"Hooks\" aside=${n.filter(s=>s.enabled).length+\" enabled / \"+n.length} />\n ${n.length?u`
${n.slice(0,8).map(s=>u`
\n ${s.type}\n ${s.description||s.matchSummary}\n ${s.id}\n
`)}
`:u`<${E} cta=${'$ qring hook add --tag payments --action rotate --exec \"...\"'}>No hooks registered.`}\n `}function bt({snap:t,delay:e}){return u`<${M} delay=${e}>\n <${S} icon=${m.memory} title=\"Agent Memory\" />\n
\n ${t.memoryKeys||0}\n memory key${t.memoryKeys===1?\"\":\"s\"} encrypted at ~/.config/q-ring/agent-memory.enc\n
\n `}function xt({snap:t,delay:e}){let n=t.anomalies||[];if(!n.length)return u`<${M} delay=${e} wide=${!0}>\n <${S} icon=${m.anomaly} title=\"Anomaly Alerts\" aside=\"all clear\" asideStyle=\"color:var(--green)\" />\n <${E}>No anomalies detected — quantum field is stable.\n `;let s=o=>o===\"tampered\"?\"$ qring audit:verify\":o===\"burst\"?\"$ qring audit --key \\u2026 --action read\":o===\"unusual-hour\"?'$ qring audit --action read --since \"1am\"':\"$ qring audit --limit 100\";return u`<${M} delay=${e} wide=${!0}>\n <${S} icon=${m.anomaly} title=\"Anomaly Alerts\" aside=${n.length+\" active\"} asideStyle=\"color:var(--danger)\" />\n
${n.map((o,a)=>u`
\n
${o.type}
\n
${o.description}
\n
${s(o.type)}
\n
`)}
\n `}function wt(t,e){let n=e.trim().toLowerCase();return n?n===\"expired\"?t.filter(s=>s.decay&&s.decay.isExpired):n===\"stale\"?t.filter(s=>s.decay&&s.decay.isStale&&!s.decay.isExpired):n===\"protected\"?t.filter(s=>s.requiresApproval):t.filter(s=>[s.key,s.scope,s.type,s.provider||\"\",s.defaultEnv||\"\",(s.environments||[]).join(\" \"),(s.tags||[]).join(\" \")].join(\" \").toLowerCase().indexOf(n)!==-1):t.slice()}function Ct(t,e){let n=e.dir===\"asc\"?1:-1;return t.sort((s,o)=>{let a,l;switch(e.col){case\"key\":a=s.key,l=o.key;break;case\"scope\":a=s.scope,l=o.scope;break;case\"env\":a=s.defaultEnv||(s.environments||[])[0]||\"\",l=o.defaultEnv||(o.environments||[])[0]||\"\";break;case\"type\":a=s.type,l=o.type;break;case\"decay\":a=s.decay&&s.decay.secondsRemaining!=null?s.decay.secondsRemaining:1/0,l=o.decay&&o.decay.secondsRemaining!=null?o.decay.secondsRemaining:1/0;break;case\"tags\":a=(s.tags||[]).join(\",\"),l=(o.tags||[]).join(\",\");break;default:a=s.lastAccessedAt?new Date(s.lastAccessedAt).getTime():0,l=o.lastAccessedAt?new Date(o.lastAccessedAt).getTime():0}return al?1*n:0})}function St({snap:t,query:e,setQuery:n,sort:s,setSort:o,searchRef:a}){let l=t.secrets||[],d=te(()=>Ct(wt(l,e),s),[l,e,s]),_=[{id:\"key\",label:\"Key\"},{id:\"scope\",label:\"Scope\"},{id:\"env\",label:\"Env\"},{id:\"type\",label:\"Type\"},{id:\"decay\",label:\"Decay\"},{id:\"tags\",label:\"Tags\"},{id:\"lastAccessedAt\",label:\"Last read\"}],c=i=>{s.col===i?o({col:i,dir:s.dir===\"asc\"?\"desc\":\"asc\"}):o({col:i,dir:i===\"key\"?\"asc\":\"desc\"})};return u`
<${M} wide=${!0} style=\"margin-bottom:16px\">\n <${S} icon=${m.key} title=\"Secrets\" aside=${d.length+\" of \"+l.length} />\n
\n \n \n \n \n \n
\n
\n ${d.length?u`\n ${_.map(i=>u``)}\n ${d.map(i=>{let r=i.type===\"superposition\"?(i.environments||[]).map(f=>u`${f}${f===i.defaultEnv?\" \\u2713\":\"\"}`):u``,p=i.decay&&i.decay.timeRemaining?(()=>{let f=Math.min(i.decay.lifetimePercent,100),C=Qe(f,i.decay.isExpired);return u`${i.decay.isExpired?\"expired\":i.decay.timeRemaining}`})():u``,v=i.tags||[];return u`\n \n \n \n \n \n \n \n `})}\n
c(i.id)}>${i.label}${s.dir===\"asc\"?\"\\u25B2\":\"\\u25BC\"}
${i.key}${i.requiresApproval?u`<${j} svg=${m.lock} />`:null}${i.scope}${r}${i.type}${p}${v.slice(0,3).map(f=>u`${f}`)}${v.length>3?\"+\"+(v.length-3):\"\"}${Ze(i.lastAccessedAt)}
`:u`
No secrets match the filter.
`}\n
\n
`}function Mt({snap:t,delay:e,filter:n,setFilter:s}){let o=t.audit||[],a=t.auditMetrics||{byAction:{},bySource:{}},l=o.filter(i=>{if(n.action&&i.action!==n.action||n.source&&i.source!==n.source)return!1;let r=n.text.trim().toLowerCase();return!(r&&[i.key||\"\",i.action,i.source,i.detail||\"\",i.scope||\"\",i.env||\"\"].join(\" \").toLowerCase().indexOf(r)===-1)}),d=[\"read\",\"write\",\"delete\",\"rotate\",\"generate\",\"entangle\",\"tunnel\",\"teleport\",\"approve\",\"revoke\",\"policy_deny\",\"collapse\",\"export\"],_=[\"cli\",\"mcp\",\"agent\",\"ci\",\"hook\",\"api\"],c=(i,r)=>s({...n,[i]:n[i]===r?\"\":r});return u`<${M} delay=${e} wide=${!0}>\n <${S} icon=${m.audit} title=\"Audit Log (24h)\" aside=${l.length+\" of \"+o.length+\" shown \\xB7 window \"+(a.windowSeconds||86400)/3600+\"h\"} />\n
\n ${d.filter(i=>a.byAction[i]).length?d.filter(i=>a.byAction[i]).map(i=>u`c(\"action\",i)}>${i}${a.byAction[i]}`):u`no actions`}\n
\n ${_.filter(i=>a.bySource[i]).length?u`
${_.filter(i=>a.bySource[i]).map(i=>u`c(\"source\",i)}>${i}${a.bySource[i]}`)}
`:null}\n
\n \n \n
\n ${l.length?u`
${l.slice(0,80).map((i,r)=>u`
\n ${Xe(i.timestamp)}\n ${i.action}\n ${i.source}\n ${i.key||\"\\u2014\"} ${i.detail||\"\"}\n
`)}
`:u`<${E}>No audit events match the filter.`}\n `}function At(){let[t,e]=q(null),[n,s]=q(!1),[o,a]=q(\"connecting\"),[l,d]=q(\"\"),[_,c]=q({col:\"lastAccessedAt\",dir:\"desc\"}),[i,r]=q({action:\"\",source:\"\",text:\"\"}),[,p]=q(0),v=fe(n);v.current=n;let f=fe(null),C=ye($=>{try{e(typeof $==\"string\"?JSON.parse($):$)}catch{}},[]),k=ye(()=>{fetch(\"/api/status\"+he,{cache:\"no-store\"}).then($=>$.json()).then(C).catch(()=>{})},[C]);ee(()=>{let $=new EventSource(\"/events\"+he);return $.onopen=()=>a(\"live\"),$.onmessage=b=>{v.current||C(b.data)},$.onerror=()=>a(\"reconnecting\"),()=>$.close()},[C]),ee(()=>{let $=setInterval(()=>p(b=>b+1),5e3);return()=>clearInterval($)},[]),ee(()=>{let $=b=>{let T=b.target&&b.target.tagName;if(T===\"INPUT\"||T===\"TEXTAREA\"){b.key===\"Escape\"&&b.target.blur();return}b.key===\"/\"?(b.preventDefault(),f.current&&f.current.focus()):b.key===\"p\"||b.key===\"P\"?s(F=>!F):(b.key===\"r\"||b.key===\"R\")&&k()};return document.addEventListener(\"keydown\",$),()=>document.removeEventListener(\"keydown\",$)},[k]);let y=t?\"updated \"+Ze(t.timestamp):\"\\u2014\";if(!t)return u`
\n <${Ge} snap=${null} paused=${n} connState=${o} onPause=${()=>s($=>!$)} onRefresh=${k} lastUpdate=${y} />\n

Connecting to the live quantum stream…

\n
`;let h=t.auditMetrics||{total:0};return u`
\n <${Ge} snap=${t} paused=${n} connState=${o} onPause=${()=>s($=>!$)} onRefresh=${k} lastUpdate=${y} />\n <${ut} snap=${t} />\n
\n <${_t} snap=${t} delay=${0} />\n <${pt} snap=${t} delay=${60} />\n <${vt} snap=${t} delay=${120} />\n <${ft} snap=${t} delay=${180} />\n
\n <${St} snap=${t} query=${l} setQuery=${d} sort=${_} setSort=${c} searchRef=${f} />\n
\n <${yt} snap=${t} delay=${0} />\n <${ht} snap=${t} delay=${60} />\n <${$t} snap=${t} delay=${120} />\n <${mt} snap=${t} delay=${180} />\n
\n
\n <${gt} snap=${t} delay=${0} />\n <${kt} snap=${t} delay=${60} />\n <${bt} snap=${t} delay=${120} />\n
\n
\n <${xt} snap=${t} delay=${0} />\n <${Mt} snap=${t} delay=${60} filter=${i} setFilter=${r} />\n
\n

\n ${\"q-ring v\"+t.version+\" \\xB7 snapshot \"+Xe(t.timestamp)+\" \\xB7 \"+(h.total||0)+\" audit events in last 24h\"}\n · keyboard: / search · P pause · R refresh\n

\n
`}qe(u`<${At} />`,document.getElementById(\"app\"));})();"; diff --git a/src/core/dashboard-html.ts b/src/core/dashboard-html.ts index b934a9a..fbc1ec2 100644 --- a/src/core/dashboard-html.ts +++ b/src/core/dashboard-html.ts @@ -2,8 +2,11 @@ * Self-contained HTML dashboard for q-ring quantum status. * * Matches the gh-pages site design system: deep-navy palette, neon cyan→violet - * SVG icons, glassmorphism cards. Zero dependencies — inline CSS + vanilla JS - * with EventSource for SSE. + * SVG icons, glassmorphism cards. The page is fully self-contained and offline: + * inline CSS plus an inlined Preact + htm client bundle (see DASHBOARD_CLIENT, + * generated from src/dashboard-client/main.js). Rendering is VDOM-diffed so the + * SSE stream updates data in place — entrance animations run once on mount + * instead of replaying on every tick. * * Layout: * 1. Sticky header with brand, version, project path, live/paused state, @@ -12,14 +15,21 @@ * protected, hooks, approvals, anomalies, audit-24h). * 3. Health donut + Environment + Manifest + Policy summary cards. * 4. Searchable + sortable secrets table (key, scope, env, type, decay, - * tags, last accessed). Click a row to inspect details. + * tags, last accessed). * 5. Quantum side panels — Decay, Superposition, Entanglement, Tunnels. * 6. Governance — Approvals, Hooks, Memory. * 7. Anomalies (full-width). * 8. Audit log with action+source+text filters and an action-count strip. */ -export function getDashboardHtml(): string { +import { DASHBOARD_CLIENT } from "./dashboard-client.js"; + +/** Escape a value for safe embedding inside an inline + + `; } diff --git a/src/core/dashboard.ts b/src/core/dashboard.ts index 029ecbb..d63249b 100644 --- a/src/core/dashboard.ts +++ b/src/core/dashboard.ts @@ -6,6 +6,7 @@ */ import { createServer, type IncomingMessage, type ServerResponse, type Server } from "node:http"; +import { randomBytes, timingSafeEqual } from "node:crypto"; import { listSecrets } from "./keyring.js"; import { checkDecay, type DecayStatus, type QuantumEnvelope } from "./envelope.js"; import { listEntanglements, type EntanglementPair } from "./entanglement.js"; @@ -385,14 +386,25 @@ export interface DashboardServerOptions { port?: number; } +function timingSafeStringEqual(a: string, b: string): boolean { + const ab = Buffer.from(a, "utf8"); + const bb = Buffer.from(b, "utf8"); + if (ab.length !== bb.length) return false; + return timingSafeEqual(ab, bb); +} + export function startDashboardServer( options: DashboardServerOptions = {}, -): { port: number; close: () => void; server: Server } { +): { port: number; token: string; url: string; close: () => void; server: Server } { const port = options.port ?? 9876; const clients = new Set(); let intervalHandle: ReturnType | null = null; - const html = getDashboardHtml(); + // Per-launch bearer token. The dashboard binds to 127.0.0.1, but on a shared + // host any local user/process could otherwise read key names, the audit log, + // and approvals. Every route requires this token (passed as ?token=). + const token = randomBytes(24).toString("base64url"); + const html = getDashboardHtml(token); function broadcast() { const snapshot = collectSnapshot(); @@ -417,14 +429,24 @@ export function startDashboardServer( const server = createServer((req: IncomingMessage, res: ServerResponse) => { let pathname: string; + let query: URLSearchParams; try { - ({ pathname } = new URL(req.url ?? "/", "http://127.0.0.1")); + const parsed = new URL(req.url ?? "/", "http://127.0.0.1"); + pathname = parsed.pathname; + query = parsed.searchParams; } catch { res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" }); res.end("Bad Request: invalid URL"); return; } + const provided = query.get("token") ?? ""; + if (!timingSafeStringEqual(provided, token)) { + res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" }); + res.end("Forbidden: missing or invalid token"); + return; + } + if (pathname === "/events") { res.writeHead(200, { "Content-Type": "text/event-stream", @@ -462,6 +484,8 @@ export function startDashboardServer( return { port, + token, + url: `http://127.0.0.1:${port}/?token=${token}`, close: () => { if (intervalHandle) clearInterval(intervalHandle); for (const res of clients) { diff --git a/src/core/exec.ts b/src/core/exec.ts index d244804..4ebcffb 100644 --- a/src/core/exec.ts +++ b/src/core/exec.ts @@ -67,6 +67,12 @@ export interface ExecResult { stderr: string; } +/** + * Best-effort output redaction: replaces verbatim occurrences of known secret + * values (>5 chars) in a child process's stdout/stderr. This is a safety net, + * NOT a guarantee — it cannot catch secrets that the child has transformed + * (base64/hex/URL-encoded, split across writes beyond the tail window, etc.). + */ export class RedactionTransform extends Transform { private patterns: { value: string; replacement: string }[] = []; private tail: string = ""; @@ -213,7 +219,12 @@ export async function execCommand(opts: ExecOptions): Promise { "curl", "wget", "ping", "nc", "netcat", "ssh", "telnet", "ftp", "dig", "nslookup", ]); - if (profile.allowNetwork === false && networkTools.has(opts.command)) { + // Compare on the command basename so absolute paths like /usr/bin/curl are + // still caught. (Interpreter-based egress — python -c, node -e — is out of + // scope here; this is best-effort, not a hard sandbox.) + const commandBase = opts.command.split(/[\\/]/).pop() ?? opts.command; + + if (profile.allowNetwork === false && networkTools.has(commandBase)) { const msg = `[QRING] Execution blocked: network access is disabled for profile "${profile.name}", command "${opts.command}" is considered network-related`; if (opts.captureOutput) { return resolve({ code: 126, stdout: "", stderr: msg }); diff --git a/src/core/import.ts b/src/core/import.ts index af90ace..9854039 100644 --- a/src/core/import.ts +++ b/src/core/import.ts @@ -51,8 +51,13 @@ export function parseDotenv(content: string): Map { }; value = value.replace(/\\([nrt"\\])/g, (_, ch) => escapeMap[ch] ?? ch); - if (value.includes("#") && !line.includes('"') && !line.includes("'")) { - value = value.split("#")[0].trim(); + // Strip inline comments only when the `#` is preceded by whitespace (the + // dotenv convention), so unquoted values like `foo#bar` are preserved. + if (!line.includes('"') && !line.includes("'")) { + const commentMatch = value.match(/\s#/); + if (commentMatch && commentMatch.index !== undefined) { + value = value.slice(0, commentMatch.index).trim(); + } } if (key) result.set(key, value); @@ -70,9 +75,18 @@ export function importDotenv( ): ImportResult { let content: string; - try { - content = readFileSync(filePathOrContent, "utf8"); - } catch { + // The file-path convenience (read from disk if the arg is a path) is only + // safe for the trusted local CLI. For MCP/agent/api callers the argument is + // always treated as literal .env content — otherwise an agent could pass a + // path like ~/.aws/credentials and exfiltrate it through the keyring. + const source = options.source ?? "cli"; + if (source === "cli") { + try { + content = readFileSync(filePathOrContent, "utf8"); + } catch { + content = filePathOrContent; + } + } else { content = filePathOrContent; } diff --git a/src/core/keyring.ts b/src/core/keyring.ts index 78fa255..d348cd5 100644 --- a/src/core/keyring.ts +++ b/src/core/keyring.ts @@ -594,6 +594,24 @@ export function exportSecrets( const decay = checkDecay(entry.envelope); if (decay.isExpired) continue; + // Honor the approval gate on bulk reads too — otherwise export_secrets + // would surface approval-protected values to an MCP agent that getSecret + // would have denied. Silently skip (consistent with policy filtering). + if ( + source === "mcp" && + entry.envelope.meta.requiresApproval && + !hasApproval(entry.key, entry.scope) + ) { + logAudit({ + action: "read", + key: entry.key, + scope: entry.scope, + source, + detail: "blocked: requires user approval (export)", + }); + continue; + } + const value = collapseValue(entry.envelope, env); if (value !== null) { rawValues.set(entry.key, value); diff --git a/src/core/noise.ts b/src/core/noise.ts index ad2739c..dddd19b 100644 --- a/src/core/noise.ts +++ b/src/core/noise.ts @@ -82,20 +82,32 @@ export function generateSecret(opts: NoiseOptions = {}): string { case "password": { const len = opts.length ?? 24; - let pw = randomString(PASSWORD_CHARS, len); - // Guarantee at least one of each class - const hasUpper = /[A-Z]/.test(pw); - const hasLower = /[a-z]/.test(pw); - const hasDigit = /[0-9]/.test(pw); - const hasSpecial = /[^A-Za-z0-9]/.test(pw); - - if (!hasUpper) pw = replaceAt(pw, randomInt(len), randomString("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1)); - if (!hasLower) pw = replaceAt(pw, randomInt(len), randomString("abcdefghijklmnopqrstuvwxyz", 1)); - if (!hasDigit) pw = replaceAt(pw, randomInt(len), randomString("0123456789", 1)); - if (!hasSpecial) pw = replaceAt(pw, randomInt(len), randomString("!@#$%^&*()-_=+", 1)); - - return pw; + // Construct from one guaranteed char per class, fill the rest, then + // shuffle. This guarantees every class is present (when len ≥ #classes) + // without any fixup pass that could clobber an already-placed class. + const classCharsets = [ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "abcdefghijklmnopqrstuvwxyz", + "0123456789", + "!@#$%^&*()-_=+", + ]; + const guaranteed = classCharsets + .slice(0, Math.min(classCharsets.length, len)) + .map((cs) => randomString(cs, 1)); + const remaining = Math.max(0, len - guaranteed.length); + const chars = [ + ...guaranteed, + ...(remaining > 0 ? randomString(PASSWORD_CHARS, remaining).split("") : []), + ]; + + // Fisher-Yates shuffle backed by the CSPRNG. + for (let i = chars.length - 1; i > 0; i--) { + const j = randomInt(i + 1); + [chars[i], chars[j]] = [chars[j], chars[i]]; + } + + return chars.join(""); } default: @@ -103,10 +115,6 @@ export function generateSecret(opts: NoiseOptions = {}): string { } } -function replaceAt(str: string, index: number, char: string): string { - return str.slice(0, index) + char + str.slice(index + 1); -} - /** * Estimate the entropy of a secret in bits. */ diff --git a/src/core/policy.ts b/src/core/policy.ts index 3503931..b965407 100644 --- a/src/core/policy.ts +++ b/src/core/policy.ts @@ -6,6 +6,8 @@ * exec allowlists, and mandatory metadata requirements. */ +import { statSync } from "node:fs"; +import { join } from "node:path"; import { readProjectConfig } from "./collapse.js"; export interface PolicyConfig { @@ -35,17 +37,43 @@ export interface PolicyDecision { policySource: string; } -let cachedPolicy: { path: string; policy: PolicyConfig } | null = null; +let cachedPolicy: { path: string; mtimeMs: number; policy: PolicyConfig } | null = null; + +/** + * Trusted policy root. When set (by the MCP server at startup), all policy + * resolution is anchored here and ignores caller-supplied projectPath. This + * prevents an MCP agent from escaping project governance by pointing + * projectPath at a directory that has no (or a weaker) `.q-ring.json`. + */ +let policyRoot: string | null = null; + +export function setPolicyRoot(root: string): void { + policyRoot = root; + cachedPolicy = null; +} + +function resolvePolicyPath(projectPath?: string): string { + return policyRoot ?? projectPath ?? process.cwd(); +} + +function configMtime(pp: string): number { + try { + return statSync(join(pp, ".q-ring.json")).mtimeMs; + } catch { + return 0; // file absent — stable sentinel + } +} export function loadPolicy(projectPath?: string): PolicyConfig { - const pp = projectPath ?? process.cwd(); - if (cachedPolicy && cachedPolicy.path === pp) { + const pp = resolvePolicyPath(projectPath); + const mtimeMs = configMtime(pp); + if (cachedPolicy && cachedPolicy.path === pp && cachedPolicy.mtimeMs === mtimeMs) { return cachedPolicy.policy; } const config = readProjectConfig(pp); const policy: PolicyConfig = (config as any)?.policy ?? {}; - cachedPolicy = { path: pp, policy }; + cachedPolicy = { path: pp, mtimeMs, policy }; return policy; } @@ -162,7 +190,15 @@ export function checkExecPolicy(command: string, projectPath?: string): PolicyDe if (!policy.exec) return { allowed: true, policySource: "no-policy" }; if (policy.exec.denyCommands) { - const denied = policy.exec.denyCommands.find((d) => command.includes(d)); + // Match on token/path boundaries so denying "rm" does not also block + // "charm" or "npm", while still catching "/usr/bin/rm" and "rm -rf". + const denied = policy.exec.denyCommands.find((d) => { + const pattern = new RegExp( + `(^|[\\s/])${d.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`, + "i", + ); + return pattern.test(command); + }); if (denied) { return { allowed: false, @@ -173,7 +209,8 @@ export function checkExecPolicy(command: string, projectPath?: string): PolicyDe } if (policy.exec.allowCommands) { - const allowed = policy.exec.allowCommands.some((a) => command.startsWith(a)); + const normalized = command.trimStart(); + const allowed = policy.exec.allowCommands.some((a) => normalized.startsWith(a)); if (!allowed) { return { allowed: false, diff --git a/src/core/ssrf.ts b/src/core/ssrf.ts index a13adb5..a76e9c2 100644 --- a/src/core/ssrf.ts +++ b/src/core/ssrf.ts @@ -7,6 +7,7 @@ import { lookup } from "node:dns/promises"; import * as dns from "node:dns"; +import { lookup as dnsLookup, type LookupAddress } from "node:dns"; import { isIPv4, isIPv6 } from "node:net"; /** `lookupSync` exists at runtime (Node 18+); some @types/node versions omit it from typings. */ @@ -73,6 +74,56 @@ export async function checkSSRF(url: string): Promise { return null; } +type GuardedLookupCallback = ( + err: NodeJS.ErrnoException | null, + address: string | LookupAddress[], + family?: number, +) => void; + +/** + * A `lookup` function for http(s).request that re-validates the resolved + * address at connection time. This closes the DNS-rebinding TOCTOU window + * where a hostname passes {@link checkSSRF} but resolves to a private/loopback + * address moments later when the socket actually connects. Fails closed. + */ +export function guardedLookup( + hostname: string, + options: dns.LookupOptions, + callback: GuardedLookupCallback, +): void { + if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") { + (dnsLookup as (h: string, o: dns.LookupOptions, cb: GuardedLookupCallback) => void)( + hostname, + options, + callback, + ); + return; + } + + (dnsLookup as (h: string, o: dns.LookupOptions, cb: GuardedLookupCallback) => void)( + hostname, + options, + (err, address, family) => { + if (err) return callback(err, address, family); + const list = Array.isArray(address) + ? address + : [{ address, family: family ?? 0 }]; + for (const a of list) { + if (isPrivateIP(a.address)) { + const blocked: NodeJS.ErrnoException = Object.assign( + new Error( + `Blocked: "${hostname}" resolved to private address ${a.address} at connect time.`, + ), + { code: "EQRINGSSRF" }, + ); + return callback(blocked, address, family); + } + } + callback(null, address, family); + }, + ); +} + /** * Sync SSRF check — validates IP literals only (no DNS resolution). * Suitable for sync contexts where async DNS lookup isn't possible. diff --git a/src/core/teleport.ts b/src/core/teleport.ts index f18445e..65a0d77 100644 --- a/src/core/teleport.ts +++ b/src/core/teleport.ts @@ -19,7 +19,10 @@ const KEY_LENGTH = 32; /** NIST / OpenSSL recommendation for AES-GCM (96-bit nonce). */ const IV_LENGTH = 12; const SALT_LENGTH = 32; -const PBKDF2_ITERATIONS = 100000; +/** OWASP-recommended floor for PBKDF2-HMAC-SHA512 (2023). */ +const PBKDF2_ITERATIONS = 210000; +/** Bundles without an explicit `iter` predate the bump; decrypt at the old cost. */ +const LEGACY_PBKDF2_ITERATIONS = 100000; export interface TeleportBundle { /** Format version */ @@ -36,6 +39,8 @@ export interface TeleportBundle { createdAt: string; /** Number of secrets in the bundle */ count: number; + /** PBKDF2 iteration count used for key derivation (absent = legacy 100k). */ + iter?: number; } export interface TeleportPayload { @@ -52,6 +57,7 @@ export const TeleportBundleSchema = z.object({ tag: z.string(), createdAt: z.string(), count: z.number(), + iter: z.number().optional(), }); export const TeleportPayloadSchema = z.object({ @@ -66,8 +72,12 @@ export const TeleportPayloadSchema = z.object({ exportedBy: z.string().optional(), }); -function deriveKey(passphrase: string, salt: Buffer): Buffer { - return pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512"); +function deriveKey( + passphrase: string, + salt: Buffer, + iterations: number = PBKDF2_ITERATIONS, +): Buffer { + return pbkdf2Sync(passphrase, salt, iterations, KEY_LENGTH, "sha512"); } /** @@ -85,7 +95,7 @@ export function teleportPack( const plaintext = JSON.stringify(payload); const salt = randomBytes(SALT_LENGTH); const iv = randomBytes(IV_LENGTH); - const key = deriveKey(passphrase, salt); + const key = deriveKey(passphrase, salt, PBKDF2_ITERATIONS); const cipher = createCipheriv(ALGORITHM, key, iv); const encrypted = Buffer.concat([ @@ -102,6 +112,7 @@ export function teleportPack( tag: tag.toString("base64"), createdAt: new Date().toISOString(), count: secrets.length, + iter: PBKDF2_ITERATIONS, }; return Buffer.from(JSON.stringify(bundle)).toString("base64"); @@ -140,7 +151,7 @@ export function teleportUnpack( const iv = Buffer.from(bundle.iv, "base64"); const tag = Buffer.from(bundle.tag, "base64"); const encrypted = Buffer.from(bundle.data, "base64"); - const key = deriveKey(passphrase, salt); + const key = deriveKey(passphrase, salt, bundle.iter ?? LEGACY_PBKDF2_ITERATIONS); const decipher = createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(tag); diff --git a/src/dashboard-client/main.js b/src/dashboard-client/main.js new file mode 100644 index 0000000..d2c74af --- /dev/null +++ b/src/dashboard-client/main.js @@ -0,0 +1,665 @@ +/** + * q-ring status dashboard — browser client (Preact + htm). + * + * Bundled into `src/core/dashboard-client.ts` by scripts/build-dashboard-client.mjs + * and inlined into the served HTML. Reads its auth token from window.__QRING__. + * + * Rendering is component + VDOM based: the SSE stream only updates the data, + * Preact diffs the DOM, so entrance animations (.card cardReveal) run once on + * mount and the page no longer "reloads" on every 5s tick. Input focus/caret + * and scroll positions are preserved automatically because DOM nodes persist. + */ +import { h, render } from "preact"; +import { useState, useEffect, useRef, useMemo, useCallback } from "preact/hooks"; +import htm from "htm"; + +const html = htm.bind(h); + +const TOKEN = (window.__QRING__ && window.__QRING__.token) || ""; +const Q = TOKEN ? "?token=" + encodeURIComponent(TOKEN) : ""; + +/* --- SVG icon library (neon gradient via #neon-grad in the page) --- */ +const ic = (path) => + '' + + path + + ""; +const icons = { + health: ic(''), + environment: ic(''), + decay: ic(''), + superposition: ic(''), + entangle: ic(''), + tunnel: ic(''), + anomaly: ic(''), + audit: ic(''), + key: ic(''), + hook: ic(''), + approve: ic(''), + manifest: ic(''), + policy: ic(''), + memory: ic(''), + folder: '', + search: '', + lock: '', +}; + +/* --- Formatters --- */ +function envClass(e) { + if (!e) return "env-default"; + const k = e.toLowerCase(); + if (k === "prod" || k === "production") return "env-prod"; + if (k === "staging" || k === "stage") return "env-staging"; + if (k === "dev" || k === "development") return "env-dev"; + if (k === "test" || k === "testing") return "env-test"; + return "env-default"; +} +function decayColor(pct, expired) { + if (expired) return "var(--danger)"; + if (pct >= 90) return "var(--danger)"; + if (pct >= 75) return "var(--warning)"; + return "var(--accent)"; +} +function fmtTime(ts) { + const d = new Date(ts); + return ( + String(d.getHours()).padStart(2, "0") + ":" + + String(d.getMinutes()).padStart(2, "0") + ":" + + String(d.getSeconds()).padStart(2, "0") + ); +} +function fmtRelative(ts) { + if (!ts) return "—"; + const diff = Math.floor((Date.now() - new Date(ts).getTime()) / 1000); + if (isNaN(diff)) return "—"; + if (diff < 5) return "just now"; + if (diff < 60) return diff + "s ago"; + if (diff < 3600) return Math.floor(diff / 60) + "m ago"; + if (diff < 86400) return Math.floor(diff / 3600) + "h ago"; + return Math.floor(diff / 86400) + "d ago"; +} +function fmtDuration(seconds) { + if (seconds == null) return "—"; + if (seconds < 0) return "expired"; + if (seconds < 60) return seconds + "s"; + if (seconds < 3600) return Math.floor(seconds / 60) + "m"; + if (seconds < 86400) return Math.floor(seconds / 3600) + "h"; + return Math.floor(seconds / 86400) + "d"; +} + +/* --- Primitives --- */ +// display:contents wrapper so the injected SVG participates in flex layout +// exactly as the old inline string did. +const Ico = ({ svg }) => + html``; + +function CardTitle({ icon, title, aside, asideStyle }) { + return html`
+ <${Ico} svg=${icon} />${title} + ${aside != null + ? html`${aside}` + : null} +
`; +} + +function Card({ delay = 0, wide = false, style = "", children }) { + const cls = "card" + (wide ? " grid-wide" : ""); + const st = "animation-delay:" + delay + "ms" + (style ? ";" + style : ""); + return html`
${children}
`; +} + +function Empty({ children, cta }) { + return html`
${children}${cta ? html`${cta}` : null}
`; +} + +/* --- Header --- */ +function Header({ snap, paused, connState, onPause, onRefresh, lastUpdate }) { + const connText = paused + ? "paused" + : connState === "live" + ? "live" + : connState === "reconnecting" + ? "reconnecting…" + : "connecting…"; + const dotCls = + "status-dot" + + (paused ? " paused" : connState === "reconnecting" ? " disconnected" : ""); + return html` +
+
+

+ + q-ring + quantum status +

+ + + ${" v" + (snap ? snap.version : "")} + + + <${Ico} svg=${icons.folder} /> ${snap ? snap.projectPath : ""} + +
+
+ ${lastUpdate} + + + + + JSON + + ${connText} +
+
+ `; +} + +/* --- KPI strip --- */ +function KpiStrip({ snap }) { + const s = snap; + const m = s.auditMetrics || { total: 0, byAction: {}, topRead: [] }; + const reads = m.byAction.read || 0; + const writes = (m.byAction.write || 0) + (m.byAction.delete || 0) + (m.byAction.rotate || 0); + const denied = m.byAction.policy_deny || 0; + const enabledHooks = (s.hooks || []).filter((h) => h.enabled).length; + const validApprovals = (s.approvals || []).filter((a) => a.valid).length; + const anomalies = (s.anomalies || []).length; + const items = [ + { label: "Secrets", icon: icons.key, value: s.health.total, sub: s.health.healthy + " healthy · " + s.health.stale + " stale · " + s.health.expired + " expired", cls: "" }, + { label: "Environment", icon: icons.environment, value: s.environment ? s.environment.env : "none", sub: s.environment ? "via " + s.environment.source : "no env detected", cls: s.environment ? "" : "dim" }, + { label: "Protected", icon: icons.lock, value: s.protectedCount, sub: "require approval", cls: s.protectedCount > 0 ? "warning" : "dim" }, + { label: "Approvals", icon: icons.approve, value: validApprovals, sub: "active grants", cls: validApprovals > 0 ? "green" : "dim" }, + { label: "Hooks", icon: icons.hook, value: enabledHooks, sub: "enabled / " + (s.hooks || []).length + " total", cls: enabledHooks > 0 ? "" : "dim" }, + { label: "Reads (24h)", icon: icons.audit, value: reads, sub: writes + " writes · " + denied + " denied", cls: "" }, + { label: "Anomalies", icon: icons.anomaly, value: anomalies, sub: anomalies ? "investigate now" : "all clear", cls: anomalies ? "danger" : "green" }, + ]; + return html`
+ ${items.map( + (k) => html`
+
<${Ico} svg=${k.icon} />${k.label}
+
${k.value}
+
${k.sub}
+
`, + )} +
`; +} + +/* --- Overview cards --- */ +function HealthCard({ snap, delay }) { + const h = snap.health; + const total = h.total || 1; + const r = 42, circ = 2 * Math.PI * r; + const slices = [ + { v: h.healthy, c: "var(--accent)" }, + { v: h.stale, c: "var(--warning)" }, + { v: h.expired, c: "var(--danger)" }, + { v: h.noDecay, c: "var(--text-dim)" }, + ]; + let offset = 0; + const rings = slices.map((sl, i) => { + const len = (sl.v / total) * circ; + const ring = html``; + offset += len; + return ring; + }); + const sc = snap.scopes || { global: 0, project: 0, team: 0, org: 0 }; + return html`<${Card} delay=${delay}> + <${CardTitle} icon=${icons.health} title="Health Summary" aside=${h.healthy + "/" + h.total + " healthy"} /> +
+
+ ${rings} +
${h.total}secrets
+
+
+
Healthy ${h.healthy}
+
Stale ${h.stale}
+
Expired ${h.expired}
+
No decay ${h.noDecay}
+
+
+
+ ${["global", "project", "team", "org"].filter((k) => sc[k]).map( + (k) => html`${sc[k]}${k}`, + )} +
+ `; +} + +function EnvCard({ snap, delay }) { + const env = snap.environment; + return html`<${Card} delay=${delay}> + <${CardTitle} icon=${icons.environment} title="Environment" /> + ${env + ? html`
${env.env}detected via ${env.source}
` + : html`<${Empty} cta="$ qring env --project-path .">No environment detected.`} + `; +} + +function ManifestCard({ snap, delay }) { + const m = snap.manifest; + if (!m) { + return html`<${Card} delay=${delay}> + <${CardTitle} icon=${icons.manifest} title="Manifest" /> + <${Empty} cta=${html`$ qring wizard `}>No .q-ring.json manifest in this project. + `; + } + const ok = m.required - m.missing.length - m.expired.length - m.stale.length; + const okPct = m.required ? Math.max(0, Math.round((ok / m.required) * 100)) : 100; + const fillCol = m.missing.length ? "var(--danger)" : m.expired.length || m.stale.length ? "var(--warning)" : "var(--green)"; + const group = (label, keys, cls) => + html`
${label} (${keys.length})
${keys.map((k) => html`${k}`)}
`; + const hasIssues = m.missing.length || m.expired.length || m.stale.length; + return html`<${Card} delay=${delay}> + <${CardTitle} icon=${icons.manifest} title="Manifest" aside=${m.required + " required / " + m.declared + " declared"} /> +
+
healthy
${okPct}%
+
+
+ ${m.missing.length ? group("Missing required", m.missing, "miss") : null} + ${m.expired.length ? group("Expired", m.expired, "exp") : null} + ${m.stale.length ? group("Stale", m.stale, "exp") : null} + ${!hasIssues ? html`
All required keys present and healthy.
` : null} +
+ `; +} + +function PolicyCard({ snap, delay }) { + const p = snap.policy || { counts: {} }; + const c = p.counts || {}; + const has = p.hasMcpPolicy || p.hasExecPolicy || p.hasSecretPolicy; + if (!has) { + return html`<${Card} delay=${delay}> + <${CardTitle} icon=${icons.policy} title="Policy" /> + <${Empty} cta=${html`add policy to .q-ring.json`}>No governance policy declared. + `; + } + const Row = ({ label, value }) => + html`
${label}${value || "—"}
`; + const aside = [p.hasMcpPolicy ? "mcp" : "", p.hasExecPolicy ? "exec" : "", p.hasSecretPolicy ? "secrets" : ""].filter(Boolean).join(" · ") || "none"; + return html`<${Card} delay=${delay}> + <${CardTitle} icon=${icons.policy} title="Policy" aside=${aside} /> +
+ <${Row} label="MCP allow tools" value=${c.allowTools} /> + <${Row} label="MCP deny tools" value=${c.denyTools} /> + <${Row} label="Denied keys" value=${c.deniedKeys} /> + <${Row} label="Denied tags" value=${c.deniedTags} /> + <${Row} label="Exec allow" value=${c.allowCommands} /> + <${Row} label="Exec deny" value=${c.denyCommands} /> + <${Row} label="Tag → approval" value=${c.requireApprovalForTags} /> + <${Row} label="Tag → rotation" value=${c.requireRotationFormatForTags} /> + ${p.maxTtlSeconds ? html`<${Row} label="Max TTL" value=${fmtDuration(p.maxTtlSeconds)} />` : null} + ${p.maxRuntimeSeconds ? html`<${Row} label="Max exec runtime" value=${fmtDuration(p.maxRuntimeSeconds)} />` : null} +
+ `; +} + +/* --- Quantum cards --- */ +function DecayCard({ snap, delay }) { + const withDecay = (snap.secrets || []) + .filter((x) => x.decay && x.decay.timeRemaining) + .sort((a, b) => (a.decay.secondsRemaining || 0) - (b.decay.secondsRemaining || 0)); + return html`<${Card} delay=${delay}> + <${CardTitle} icon=${icons.decay} title="Decay Timers" aside=${withDecay.length + " tracked"} /> + ${!withDecay.length + ? html`<${Empty} cta="$ qring set KEY --ttl 86400">No secrets with decay configured.` + : html`
${withDecay.slice(0, 12).map((x) => { + const pct = Math.min(x.decay.lifetimePercent, 100); + const col = decayColor(pct, x.decay.isExpired); + const label = x.decay.isExpired ? "expired" : x.decay.timeRemaining || ""; + return html`
+ ${x.key} +
+ ${label} +
`; + })}
`} + `; +} + +function SuperpositionCard({ snap, delay }) { + const sup = (snap.secrets || []).filter((x) => x.type === "superposition" && x.environments && x.environments.length); + return html`<${Card} delay=${delay}> + <${CardTitle} icon=${icons.superposition} title="Superposition" aside=${sup.length + " multi-env"} /> + ${!sup.length + ? html`<${Empty} cta="$ qring set KEY --env prod">No secrets in superposition.` + : html`
${sup.slice(0, 14).map((x) => html`
+ ${x.key} + ${(x.environments || []).map((e) => html`${e}${e === x.defaultEnv ? " ✓" : ""}`)} +
`)}
`} + `; +} + +function EntanglementCard({ snap, delay }) { + const pairs = snap.entanglements || []; + const seen = new Set(); + const unique = pairs.filter((p) => { + const id = [p.source.service, p.source.key, p.target.service, p.target.key].sort().join("|"); + if (seen.has(id)) return false; + seen.add(id); + return true; + }); + return html`<${Card} delay=${delay}> + <${CardTitle} icon=${icons.entangle} title="Entanglement" aside=${unique.length + " pair" + (unique.length === 1 ? "" : "s")} /> + ${!unique.length + ? html`<${Empty} cta="$ qring entangle KEY_A KEY_B">No entangled secrets.` + : html`
${unique.slice(0, 12).map((p, i) => html`
+ ${p.source.key}${p.target.key} +
`)}
`} + `; +} + +function TunnelsCard({ snap, delay }) { + const tunnels = snap.tunnels || []; + return html`<${Card} delay=${delay}> + <${CardTitle} icon=${icons.tunnel} title="Quantum Tunnels" aside=${tunnels.length + " live"} /> + ${!tunnels.length + ? html`<${Empty} cta=${html`$ qring tunnel create `}>No active tunnels.` + : html`
${tunnels.map((t) => { + const rem = t.expiresAt ? Math.max(0, Math.floor((t.expiresAt - Date.now()) / 1000)) : null; + return html`
${t.id}
+ reads: ${t.accessCount}${t.maxReads ? "/" + t.maxReads : ""} + ${rem !== null ? html`expires: ${rem}s` : html`no expiry`} +
`; + })}
`} + `; +} + +/* --- Governance cards --- */ +function ApprovalsCard({ snap, delay }) { + const list = snap.approvals || []; + return html`<${Card} delay=${delay}> + <${CardTitle} icon=${icons.approve} title="Approvals" aside=${list.filter((a) => a.valid).length + " valid"} /> + ${!list.length + ? html`<${Empty} cta=${'$ qring approve KEY --for 1800 --reason "..."'}>No active approvals.` + : html`
${list.slice(0, 8).map((a) => { + const cls = a.tampered ? "tampered" : a.secondsRemaining < 300 ? "expiring" : ""; + return html`
+
${a.key}${a.tampered ? " ⚠ TAMPERED" : ""}${fmtDuration(a.secondsRemaining)} left
+
${a.reason || "(no reason)"}
+
scope:${a.scope}by:${a.grantedBy}
+
`; + })}
`} + `; +} + +function HooksCard({ snap, delay }) { + const list = snap.hooks || []; + return html`<${Card} delay=${delay}> + <${CardTitle} icon=${icons.hook} title="Hooks" aside=${list.filter((h) => h.enabled).length + " enabled / " + list.length} /> + ${!list.length + ? html`<${Empty} cta=${'$ qring hook add --tag payments --action rotate --exec "..."'}>No hooks registered.` + : html`
${list.slice(0, 8).map((h) => html`
+ ${h.type} + ${h.description || h.matchSummary} + ${h.id} +
`)}
`} + `; +} + +function MemoryCard({ snap, delay }) { + return html`<${Card} delay=${delay}> + <${CardTitle} icon=${icons.memory} title="Agent Memory" /> +
+ ${snap.memoryKeys || 0} + memory key${snap.memoryKeys === 1 ? "" : "s"} encrypted at ~/.config/q-ring/agent-memory.enc +
+ `; +} + +/* --- Anomalies --- */ +function AnomaliesCard({ snap, delay }) { + const list = snap.anomalies || []; + if (!list.length) { + return html`<${Card} delay=${delay} wide=${true}> + <${CardTitle} icon=${icons.anomaly} title="Anomaly Alerts" aside="all clear" asideStyle="color:var(--green)" /> + <${Empty}>No anomalies detected — quantum field is stable. + `; + } + const hintFor = (t) => + t === "tampered" ? "$ qring audit:verify" + : t === "burst" ? "$ qring audit --key … --action read" + : t === "unusual-hour" ? '$ qring audit --action read --since "1am"' + : "$ qring audit --limit 100"; + return html`<${Card} delay=${delay} wide=${true}> + <${CardTitle} icon=${icons.anomaly} title="Anomaly Alerts" aside=${list.length + " active"} asideStyle="color:var(--danger)" /> +
${list.map((a, i) => html`
+
${a.type}
+
${a.description}
+
${hintFor(a.type)}
+
`)}
+ `; +} + +/* --- Secrets table --- */ +function filterSecrets(rows, query) { + const q = query.trim().toLowerCase(); + if (!q) return rows.slice(); + if (q === "expired") return rows.filter((r) => r.decay && r.decay.isExpired); + if (q === "stale") return rows.filter((r) => r.decay && r.decay.isStale && !r.decay.isExpired); + if (q === "protected") return rows.filter((r) => r.requiresApproval); + return rows.filter((r) => { + const hay = [r.key, r.scope, r.type, r.provider || "", r.defaultEnv || "", (r.environments || []).join(" "), (r.tags || []).join(" ")].join(" ").toLowerCase(); + return hay.indexOf(q) !== -1; + }); +} +function sortRows(rows, sort) { + const dir = sort.dir === "asc" ? 1 : -1; + return rows.sort((a, b) => { + let av, bv; + switch (sort.col) { + case "key": av = a.key; bv = b.key; break; + case "scope": av = a.scope; bv = b.scope; break; + case "env": av = a.defaultEnv || (a.environments || [])[0] || ""; bv = b.defaultEnv || (b.environments || [])[0] || ""; break; + case "type": av = a.type; bv = b.type; break; + case "decay": av = a.decay && a.decay.secondsRemaining != null ? a.decay.secondsRemaining : Infinity; bv = b.decay && b.decay.secondsRemaining != null ? b.decay.secondsRemaining : Infinity; break; + case "tags": av = (a.tags || []).join(","); bv = (b.tags || []).join(","); break; + case "lastAccessedAt": + default: av = a.lastAccessedAt ? new Date(a.lastAccessedAt).getTime() : 0; bv = b.lastAccessedAt ? new Date(b.lastAccessedAt).getTime() : 0; + } + if (av < bv) return -1 * dir; + if (av > bv) return 1 * dir; + return 0; + }); +} + +function SecretsSection({ snap, query, setQuery, sort, setSort, searchRef }) { + const all = snap.secrets || []; + const filtered = useMemo(() => sortRows(filterSecrets(all, query), sort), [all, query, sort]); + const cols = [ + { id: "key", label: "Key" }, + { id: "scope", label: "Scope" }, + { id: "env", label: "Env" }, + { id: "type", label: "Type" }, + { id: "decay", label: "Decay" }, + { id: "tags", label: "Tags" }, + { id: "lastAccessedAt", label: "Last read" }, + ]; + const onSort = (col) => { + if (sort.col === col) setSort({ col, dir: sort.dir === "asc" ? "desc" : "asc" }); + else setSort({ col, dir: col === "key" ? "asc" : "desc" }); + }; + return html`
<${Card} wide=${true} style="margin-bottom:16px"> + <${CardTitle} icon=${icons.key} title="Secrets" aside=${filtered.length + " of " + all.length} /> +
+ + + + + +
+
+ ${!filtered.length + ? html`
No secrets match the filter.
` + : html` + ${cols.map((c) => html``)} + ${filtered.map((r) => { + const env = r.type === "superposition" + ? (r.environments || []).map((e) => html`${e}${e === r.defaultEnv ? " ✓" : ""}`) + : html``; + const decayCell = r.decay && r.decay.timeRemaining + ? (() => { + const pct = Math.min(r.decay.lifetimePercent, 100); + const col = decayColor(pct, r.decay.isExpired); + return html`${r.decay.isExpired ? "expired" : r.decay.timeRemaining}`; + })() + : html``; + const tags = r.tags || []; + return html` + + + + + + + + `; + })} +
onSort(c.id)}>${c.label}${sort.dir === "asc" ? "▲" : "▼"}
${r.key}${r.requiresApproval ? html`<${Ico} svg=${icons.lock} />` : null}${r.scope}${env}${r.type}${decayCell}${tags.slice(0, 3).map((t) => html`${t}`)}${tags.length > 3 ? "+" + (tags.length - 3) : ""}${fmtRelative(r.lastAccessedAt)}
`} +
+
`; +} + +/* --- Audit log --- */ +function AuditCard({ snap, delay, filter, setFilter }) { + const events = snap.audit || []; + const m = snap.auditMetrics || { byAction: {}, bySource: {} }; + const filtered = events.filter((e) => { + if (filter.action && e.action !== filter.action) return false; + if (filter.source && e.source !== filter.source) return false; + const txt = filter.text.trim().toLowerCase(); + if (txt) { + const hay = [e.key || "", e.action, e.source, e.detail || "", e.scope || "", e.env || ""].join(" ").toLowerCase(); + if (hay.indexOf(txt) === -1) return false; + } + return true; + }); + const allActions = ["read", "write", "delete", "rotate", "generate", "entangle", "tunnel", "teleport", "approve", "revoke", "policy_deny", "collapse", "export"]; + const allSources = ["cli", "mcp", "agent", "ci", "hook", "api"]; + const toggle = (field, val) => setFilter({ ...filter, [field]: filter[field] === val ? "" : val }); + return html`<${Card} delay=${delay} wide=${true}> + <${CardTitle} icon=${icons.audit} title="Audit Log (24h)" aside=${filtered.length + " of " + events.length + " shown · window " + (m.windowSeconds || 86400) / 3600 + "h"} /> +
+ ${allActions.filter((a) => m.byAction[a]).length + ? allActions.filter((a) => m.byAction[a]).map((a) => html` toggle("action", a)}>${a}${m.byAction[a]}`) + : html`no actions`} +
+ ${allSources.filter((src) => m.bySource[src]).length + ? html`
${allSources.filter((src) => m.bySource[src]).map((src) => html` toggle("source", src)}>${src}${m.bySource[src]}`)}
` + : null} +
+ + +
+ ${!filtered.length + ? html`<${Empty}>No audit events match the filter.` + : html`
${filtered.slice(0, 80).map((e, i) => html`
+ ${fmtTime(e.timestamp)} + ${e.action} + ${e.source} + ${e.key || "—"} ${e.detail || ""} +
`)}
`} + `; +} + +/* --- App root --- */ +function App() { + const [snap, setSnap] = useState(null); + const [paused, setPaused] = useState(false); + const [connState, setConnState] = useState("connecting"); + const [query, setQuery] = useState(""); + const [sort, setSort] = useState({ col: "lastAccessedAt", dir: "desc" }); + const [auditFilter, setAuditFilter] = useState({ action: "", source: "", text: "" }); + const [, setTick] = useState(0); // forces relative-time refresh + + const pausedRef = useRef(paused); + pausedRef.current = paused; + const searchRef = useRef(null); + + const applySnap = useCallback((data) => { + try { setSnap(typeof data === "string" ? JSON.parse(data) : data); } catch { /* ignore */ } + }, []); + + const forceRefresh = useCallback(() => { + fetch("/api/status" + Q, { cache: "no-store" }).then((r) => r.json()).then(applySnap).catch(() => {}); + }, [applySnap]); + + // SSE connection (mounted once). + useEffect(() => { + const es = new EventSource("/events" + Q); + es.onopen = () => setConnState("live"); + es.onmessage = (e) => { if (!pausedRef.current) applySnap(e.data); }; + es.onerror = () => setConnState("reconnecting"); + return () => es.close(); + }, [applySnap]); + + // Tick every 5s so relative timestamps stay accurate even while paused. + useEffect(() => { + const t = setInterval(() => setTick((n) => n + 1), 5000); + return () => clearInterval(t); + }, []); + + // Keyboard shortcuts. + useEffect(() => { + const onKey = (e) => { + const tag = e.target && e.target.tagName; + if (tag === "INPUT" || tag === "TEXTAREA") { + if (e.key === "Escape") e.target.blur(); + return; + } + if (e.key === "/") { e.preventDefault(); if (searchRef.current) searchRef.current.focus(); } + else if (e.key === "p" || e.key === "P") setPaused((p) => !p); + else if (e.key === "r" || e.key === "R") forceRefresh(); + }; + document.addEventListener("keydown", onKey); + return () => document.removeEventListener("keydown", onKey); + }, [forceRefresh]); + + const lastUpdate = snap ? "updated " + fmtRelative(snap.timestamp) : "—"; + + if (!snap) { + return html`
+ <${Header} snap=${null} paused=${paused} connState=${connState} onPause=${() => setPaused((p) => !p)} onRefresh=${forceRefresh} lastUpdate=${lastUpdate} /> +

Connecting to the live quantum stream…

+
`; + } + + const m = snap.auditMetrics || { total: 0 }; + return html`
+ <${Header} snap=${snap} paused=${paused} connState=${connState} onPause=${() => setPaused((p) => !p)} onRefresh=${forceRefresh} lastUpdate=${lastUpdate} /> + <${KpiStrip} snap=${snap} /> +
+ <${HealthCard} snap=${snap} delay=${0} /> + <${EnvCard} snap=${snap} delay=${60} /> + <${ManifestCard} snap=${snap} delay=${120} /> + <${PolicyCard} snap=${snap} delay=${180} /> +
+ <${SecretsSection} snap=${snap} query=${query} setQuery=${setQuery} sort=${sort} setSort=${setSort} searchRef=${searchRef} /> +
+ <${DecayCard} snap=${snap} delay=${0} /> + <${SuperpositionCard} snap=${snap} delay=${60} /> + <${EntanglementCard} snap=${snap} delay=${120} /> + <${TunnelsCard} snap=${snap} delay=${180} /> +
+
+ <${ApprovalsCard} snap=${snap} delay=${0} /> + <${HooksCard} snap=${snap} delay=${60} /> + <${MemoryCard} snap=${snap} delay=${120} /> +
+
+ <${AnomaliesCard} snap=${snap} delay=${0} /> + <${AuditCard} snap=${snap} delay=${60} filter=${auditFilter} setFilter=${setAuditFilter} /> +
+

+ ${"q-ring v" + snap.version + " · snapshot " + fmtTime(snap.timestamp) + " · " + (m.total || 0) + " audit events in last 24h"} + · keyboard: / search · P pause · R refresh +

+
`; +} + +render(html`<${App} />`, document.getElementById("app")); diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 9ccf072..e4803d0 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -1,8 +1,14 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { PACKAGE_VERSION } from "../version.js"; import { registerMcpTools } from "./tool-registration.js"; +import { setPolicyRoot } from "../core/policy.js"; export function createMcpServer(): McpServer { + // Anchor governance policy to the directory the operator launched the server + // in. Agents pass projectPath freely, so resolving policy from it would let a + // malicious agent escape `.q-ring.json` restrictions by pointing elsewhere. + setPolicyRoot(process.cwd()); + const server = new McpServer({ name: "q-ring", version: PACKAGE_VERSION, diff --git a/src/mcp/tools/tooling.ts b/src/mcp/tools/tooling.ts index cf55357..6439387 100644 --- a/src/mcp/tools/tooling.ts +++ b/src/mcp/tools/tooling.ts @@ -224,7 +224,7 @@ export function registerToolingTools(server: McpServer): void { // One process-scoped instance; reusing across tool invocations avoids // leaking listeners when the MCP client pings `status_dashboard` twice. - let dashboardInstance: { port: number; close: () => void } | null = null; + let dashboardInstance: { port: number; url: string; close: () => void } | null = null; server.tool( "status_dashboard", @@ -248,7 +248,7 @@ export function registerToolingTools(server: McpServer): void { if (dashboardInstance) { return text( - `Dashboard already running at http://127.0.0.1:${dashboardInstance.port}`, + `Dashboard already running at ${dashboardInstance.url}`, ); } @@ -256,7 +256,7 @@ export function registerToolingTools(server: McpServer): void { dashboardInstance = startDashboardServer({ port: params.port }); return text( - `Dashboard started at http://127.0.0.1:${dashboardInstance.port}\nOpen this URL in a browser to see live quantum status.`, + `Dashboard started at ${dashboardInstance.url}\nOpen this URL in a browser to see live quantum status. The token is required for access.`, ); }, ); diff --git a/src/utils/http-request.ts b/src/utils/http-request.ts index b78396d..43ef1e7 100644 --- a/src/utils/http-request.ts +++ b/src/utils/http-request.ts @@ -5,6 +5,8 @@ import { request as httpsRequest } from "node:https"; import { request as httpRequestPlain } from "node:http"; +import type { LookupFunction } from "node:net"; +import { guardedLookup } from "../core/ssrf.js"; export interface HttpRequestOptions { url: string; @@ -54,7 +56,13 @@ export function httpRequest(opts: HttpRequestOptions): Promise { const req = reqFn( url, - { method, headers: reqHeaders, timeout: timeoutMs }, + { + method, + headers: reqHeaders, + timeout: timeoutMs, + // Re-validate the resolved IP at connect time (DNS-rebinding guard). + lookup: guardedLookup as unknown as LookupFunction, + }, (res) => { const chunks: Buffer[] = []; let totalBytes = 0; From c75a392c699d42ddd5636470926989cc0ef18e70 Mon Sep 17 00:00:00 2001 From: I4cTime Date: Thu, 25 Jun 2026 00:18:31 -0500 Subject: [PATCH 03/16] chore(repo): add Dependabot config and PR template (#59) - Dependabot: weekly npm + github-actions updates; minor/patch grouped into prod/dev batches to cut PR noise, majors/security stay individual. - PR template with type + verification checklist matching CI gates. Co-authored-by: Claude Opus 4.8 --- .github/dependabot.yml | 39 ++++++++++++++++++++++++++++++++ .github/pull_request_template.md | 30 ++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f6b7c39 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,39 @@ +version: 2 +updates: + # npm / pnpm dependencies + - package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + labels: + - dependencies + commit-message: + prefix: "chore(deps)" + prefix-development: "chore(deps-dev)" + # Batch routine minor/patch bumps into two PRs to cut noise; security and + # major updates still open as individual PRs so they get scrutiny. + groups: + production-minor-patch: + dependency-type: production + update-types: + - minor + - patch + development-minor-patch: + dependency-type: development + update-types: + - minor + - patch + + # GitHub Actions used by the workflows + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + day: monday + labels: + - dependencies + - github-actions + commit-message: + prefix: "ci(deps)" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..bab7122 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,30 @@ + + +## Summary + + + +## Type + +- [ ] Feature +- [ ] Fix +- [ ] Security +- [ ] Docs +- [ ] Chore / CI / deps + +## Checklist + +- [ ] `pnpm run typecheck` passes +- [ ] `pnpm run lint` passes +- [ ] `pnpm run build` passes +- [ ] `pnpm run test:ci` passes +- [ ] `CHANGELOG.md` updated under `[Unreleased]` (for user-facing changes) +- [ ] Docs / README updated if behavior changed +- [ ] CLI ↔ MCP parity kept (if a command or tool was added/changed) + +## Breaking changes + + From a7fd314a4044876a2af507c22cecb42368cf91a2 Mon Sep 17 00:00:00 2001 From: I4cTime Date: Thu, 25 Jun 2026 00:30:58 -0500 Subject: [PATCH 04/16] fix(ci): drop pnpm version pin in publish.yml (conflicts with packageManager) (#65) pnpm/action-setup errors when both a `version` input and the package.json `packageManager` field are set. ci.yml was already fixed; do the same here so the release publish workflow can run. Co-authored-by: Claude Opus 4.8 --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1a24961..bdb2b18 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -42,9 +42,9 @@ jobs: # self-upgrade. Node 24's bundled npm is already >= 11.5.1, so # no upgrade is needed for trusted publishing or provenance. + # pnpm version comes from the "packageManager" field in package.json; + # do not also pin "version" here (pnpm/action-setup errors on conflict). - uses: pnpm/action-setup@v4 - with: - version: latest - run: pnpm install --frozen-lockfile From 47e1e980c5e1bc18dda7161b4b353627d3c13201 Mon Sep 17 00:00:00 2001 From: I4cTime Date: Thu, 25 Jun 2026 00:42:56 -0500 Subject: [PATCH 05/16] fix(release): sync server.json version + idempotent npm publish (#67) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v0.12.0 npm publish succeeded, but the MCP Registry step failed with 'cannot publish duplicate version' because server.json was still at 0.11.7 — sync-versions never updated it. - Bump server.json to 0.12.0 (top-level + npm package entry). - Teach sync-versions to keep server.json in lockstep going forward. - Make the npm-publish step skip when the version already exists, so the release workflow is safe to re-run (e.g. after the MCP step fails). Co-authored-by: Claude Opus 4.8 --- .github/workflows/publish.yml | 9 ++++++++- scripts/sync-versions.mjs | 18 ++++++++++++++++-- server.json | 4 ++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bdb2b18..e2d67d3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -55,7 +55,14 @@ jobs: run: pnpm run build - name: Publish to npm - run: npm publish --provenance --access public + run: | + NAME=$(node -p "require('./package.json').name") + VER=$(node -p "require('./package.json').version") + if npm view "$NAME@$VER" version >/dev/null 2>&1; then + echo "npm already has $NAME@$VER — skipping (idempotent re-run)." + else + npm publish --provenance --access public + fi - name: Install mcp-publisher run: | diff --git a/scripts/sync-versions.mjs b/scripts/sync-versions.mjs index a569746..ad39af5 100644 --- a/scripts/sync-versions.mjs +++ b/scripts/sync-versions.mjs @@ -1,6 +1,7 @@ #!/usr/bin/env node /** - * Sync version from package.json into plugin + marketplace + SECURITY.md supported table. + * Sync version from package.json into plugin + marketplace + server.json + * (MCP Registry) + SECURITY.md supported table. */ import { readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -23,6 +24,17 @@ if (marketplace.plugins?.[0]) { } writeFileSync(marketplacePath, JSON.stringify(marketplace, null, 2) + "\n", "utf8"); +// server.json drives the MCP Registry publish — keep its top-level version and +// every npm package entry in lockstep, or `mcp-publisher` rejects a stale or +// duplicate version at release time. +const serverJsonPath = join(root, "server.json"); +const serverJson = JSON.parse(readFileSync(serverJsonPath, "utf8")); +serverJson.version = v; +for (const p of serverJson.packages ?? []) { + if (p.registryType === "npm" || p.identifier === pkg.name) p.version = v; +} +writeFileSync(serverJsonPath, JSON.stringify(serverJson, null, 2) + "\n", "utf8"); + const securityPath = join(root, "SECURITY.md"); let sec = readFileSync(securityPath, "utf8"); const parts = v.split("."); @@ -34,4 +46,6 @@ sec = sec.replace( ); writeFileSync(securityPath, sec, "utf8"); -console.log(`sync-versions: set ${v} in plugin.json, marketplace.json, SECURITY.md`); +console.log( + `sync-versions: set ${v} in plugin.json, marketplace.json, server.json, SECURITY.md`, +); diff --git a/server.json b/server.json index f06224f..0af9cb2 100644 --- a/server.json +++ b/server.json @@ -6,12 +6,12 @@ "url": "https://github.com/I4cTime/quantum_ring", "source": "github" }, - "version": "0.11.7", + "version": "0.12.0", "packages": [ { "registryType": "npm", "identifier": "@i4ctime/q-ring", - "version": "0.11.7", + "version": "0.12.0", "transport": { "type": "stdio" } From ea8d9a3debbea39b9cba8531b64f31cf52953139 Mon Sep 17 00:00:00 2001 From: I4cTime Date: Sat, 11 Jul 2026 01:00:57 -0500 Subject: [PATCH 06/16] Hardening pass: workflows, plugins, CLI UX, dashboard, release automation (#74) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci: harden workflows — smoke tests, non-blocking audit, pinned mcp-publisher, brew test fix - CI: smoke-test the built qring CLI (--version/--help) and the MCP server (initialize handshake) after build; move pnpm audit to a separate non-blocking job so new upstream advisories don't fail unrelated PRs - CodeQL: also scan direct pushes to main - publish: trigger on release published (not created — drafts no longer publish), pin mcp-publisher to v1.7.9, add concurrency guard, enable pnpm cache (pnpm setup moved before setup-node) - update-homebrew: formula test now asserts the version string (qring --version prints the bare version, so asserting "qring" failed every brew test), guard tarball download with curl -f + sha check, skip commit when formula is unchanged, add concurrency guard - bug template: version placeholder 0.2.9 → 0.12.0 Co-Authored-By: Claude Fable 5 * feat(claude-plugin): make the Claude Code plugin actually installable Add claude-code-plugin/.claude-plugin/plugin.json and a repo-root .claude-plugin/marketplace.json so the plugin installs via /plugin marketplace add I4cTime/quantum_ring + /plugin install qring@q-ring instead of file copying. Components (agents/commands/skills/hooks) move from .claude/ to the plugin root to match the Claude Code plugin format; hooks get a plugin-format hooks/hooks.json using ${CLAUDE_PLUGIN_ROOT}. The project-scoped sync path still works: plugin-sync-claude.mjs maps the new layout back under .claude/ (skipping the plugin-only hooks.json) and keeps wiring hooks via settings.json. sync-versions.mjs now stamps both new manifests so the Claude plugin is version-tracked like Cursor's. Co-Authored-By: Claude Fable 5 * feat(cli): central error handler, qring has, non-TTY stdin for set - Wrap program.parseAsync() in a central handler: errors thrown by any command (policy denials, keyring backend failures) print as a one-line red message with exit code 1 instead of a raw Node stack trace; QRING_DEBUG=1 restores the full stack for bug reports - Add qring has : decay-aware existence check, exit 0/1, --quiet for exit-code-only scripting, honors --json — closes the last MCP→CLI parity gap (has_secret) - promptSecret reads stdin to EOF when it isn't a TTY, so echo "$VALUE" | qring set KEY works instead of hanging forever; empty piped input errors cleanly Co-Authored-By: Claude Fable 5 * fix(dashboard): enforce security headers, test the auth path The dashboard was documented as CSP-clean but never sent a policy. Every response now carries Content-Security-Policy (default-deny; inline script/style + same-origin SSE only, frame-ancestors 'none'), X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer (keeps the ?token= URL out of Referer headers), and Cache-Control: no-store (the HTML embeds the launch token). Adds the first dashboard server tests: missing/wrong token → 403, valid token → 200, and the security headers on both paths. Co-Authored-By: Claude Fable 5 * docs: Docker install section, CONTRIBUTING.md, changelog for unreleased changes - README: document the Dockerfile/mcp-proxy container path (previously only discoverable via docs/publishing-log.md) with a note on the no-OS-keychain limitation; add qring has example; link CONTRIBUTING.md - CONTRIBUTING.md: dev environment, branch/commit conventions, and the keep-in-sync list (parity doc, plugin manifests, dashboard bundle) - CHANGELOG: record all unreleased changes from this branch Co-Authored-By: Claude Fable 5 * feat(cli): universal --json output and destructive-op confirmations --json (global or per-command) now works on ~20 more commands — list, inspect, delete, import, audit, audit:verify, health, analyze, recall, check, env, validate (single/--all/--list-providers), rotate, tunnel list, hook list, approvals, scan, lint — all emitting { ok: true, data } via a shared emitJson helper. Previously only 4 of ~40 commands honored the advertised flag. JSON output never includes secret values. Destructive operations now confirm before proceeding: delete, forget --all, teleport unpack (when it would overwrite existing keys), wizard (when its generated keys already exist), and env:generate -o (when the output file exists). -y/--yes skips the prompt; non-TTY use without --yes is an error, never a silent yes. BREAKING for scripts that relied on unconfirmed delete/forget --all — add --yes. Also narrows teleport unpack's catch so confirm/import errors are no longer misreported as "wrong passphrase", and scan/lint/check/ audit:verify set exit code 1 on findings in JSON mode to stay script-friendly. Co-Authored-By: Claude Fable 5 * feat(plugins): generate skills from single source + parity check script cursor-plugin/skills/*/SKILL.md is now the canonical source; a new scripts/plugin-generate-skills.mjs derives the Claude Code skills (verbatim) and Kiro steering files (manual-inclusion transform with #qring- chat tags) — pnpm run plugin:gen-skills. The regenerated files in this commit reconcile the cosmetic drift (heading case, arrows) that hand-copying had accumulated across the three plugins. scripts/check-plugin-parity.mjs (pnpm run check:parity) fails when: - the plugins' command/agent/skill sets diverge - doc'd MCP tool counts don't match the registered count (44 today, verified against server.tool( registrations in src/mcp/tools) - plugin manifest versions drift from package.json - generated skill files are stale (--check mode of the generator) Co-Authored-By: Claude Fable 5 * ci: plugin-parity job and cross-platform compat matrix parity job: runs check:parity (plugin sync, tool counts, versions, generated skills) and verifies the committed dashboard client bundle matches a fresh build:dashboard. compat matrix (experimental, continue-on-error until proven green): macOS + Windows on Node 22 exercise the per-OS keyring backends for the first time; ubuntu Node 24 covers the publish toolchain; a Node 18 engines-floor leg does build + CLI smoke only (vitest 4 requires Node 20+). Co-Authored-By: Claude Fable 5 * docs: changelog + contributing notes for Phase 2 Co-Authored-By: Claude Fable 5 * feat(cli): qring doctor self-check and shell completions qring doctor diagnoses the install in one shot: keyring-backend write/read/delete probe (isolated service, never touches real secrets — this is the check that catches headless Linux without a Secret Service), audit-dir writability + hash-chain integrity, .q-ring.json validity, active policy summary, and qring-mcp resolvable on PATH (with per-OS extension handling for Windows). --json supported; exit 1 on any fail, warnings don't fail. qring completion bash|zsh|fish prints completion scripts generated by walking the live commander program — command names, aliases, subcommands, and long flags can never drift from the actual CLI. bash/zsh output passes bash -n / zsh -n. Co-Authored-By: Claude Fable 5 * feat(dashboard): entanglement graph and audit-chain integrity badge The snapshot now carries auditChain (same data as qring audit:verify), recomputed each 5s tick; the Audit Log card renders it as a live badge — green "chain intact · N events verified", red with the break position when tampered. Entangled secrets render as an SVG node/edge graph on a circular layout (nodes are unique keys, one edge per pair, labels anchored outward with full-name tooltips) instead of the flat pair list the CHANGELOG had been advertising as a graph. Falls back to the list beyond 14 keys. Styled via theme variables, aria-labelled, no new dependencies — the committed client bundle is regenerated. Tests: snapshot includes auditChain, and the served page contains the graph/badge styles and rendered badge markup. Co-Authored-By: Claude Fable 5 * feat(release): tag-driven release automation pnpm run release:prepare stages a release: bumps package.json, runs sync-versions across every manifest, rolls the CHANGELOG [Unreleased] section into [x.y.z] — date (failing if empty), runs the plugin-parity guard, and prints the commit/tag/push steps. Verified end-to-end against a repo copy. Pushing the v* tag runs the new release.yml: creates the GitHub Release idempotently with notes extracted from that version's CHANGELOG section (fails loudly if the section is missing), then chains publish.yml and update-homebrew.yml via workflow_call. The explicit chaining exists because GITHUB_TOKEN-created releases emit no events, so the release:published triggers can't fire on this path; they remain for releases created manually in the UI, and every downstream step is idempotent with a shared publish concurrency group. Co-Authored-By: Claude Fable 5 * docs: troubleshooting guide, per-editor quickstarts, releasing guide - docs/troubleshooting.md: qring doctor first, keyring backends (headless Linux/CI), MCP-not-connecting per editor with a manual handshake probe, approval gate, 0.12.0 policy-root pinning, audit log, scripting gotchas (--yes, --json, --raw, has --quiet), AGPL FAQ - docs/quickstart-{claude-code,cursor,kiro}.md: five-minute setups with example prompts per editor (the "prompt cookbooks" the README promises) - docs/releasing.md: the tag-driven flow and failure recovery - README: docs index section, doctor + completion in Quick Start - parity check now also guards the tool-count claims in the quickstarts Co-Authored-By: Claude Fable 5 * fix: track exec-with-secrets cursor skill; fix approval test on Windows Two bugs the new CI jobs caught on their first run: - The blanket *secret* pattern in .gitignore silently kept cursor-plugin/skills/exec-with-secrets/SKILL.md out of git — the canonical source for a shipped skill existed only on the maintainer's machine. Extend the gitignore allow-list to cover it and modernize the Claude plugin entries to the new plugin-root layout (the old .claude/skills paths no longer exist). Caught by the parity job. - approval.test.ts redirected the approval store via $HOME, but os.homedir() reads USERPROFILE on Windows, so the store went to the runner's real profile while the test read the temp dir (3 ENOENT failures). Set and restore both variables. Caught by the compat matrix — which otherwise passed everywhere, including the Windows and macOS keyring backends (179/182 on first-ever Windows run). Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- .claude-plugin/marketplace.json | 32 +++ .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/workflows/ci.yml | 117 +++++++++- .github/workflows/codeql.yml | 2 +- .github/workflows/publish.yml | 34 ++- .github/workflows/release.yml | 71 ++++++ .github/workflows/update-homebrew.yml | 28 ++- .gitignore | 16 +- CHANGELOG.md | 28 +++ CONTRIBUTING.md | 54 +++++ README.md | 31 +++ claude-code-plugin/.claude-plugin/plugin.json | 23 ++ claude-code-plugin/README.md | 39 +++- .../{.claude => }/agents/secret-ops.md | 0 .../{.claude => }/agents/security-auditor.md | 0 .../{.claude => }/commands/qring-analyze.md | 0 .../{.claude => }/commands/qring-dashboard.md | 0 .../{.claude => }/commands/qring-exec-safe.md | 0 .../commands/qring-health-check.md | 0 .../commands/qring-rotate-expired.md | 0 .../commands/qring-scan-secrets.md | 0 .../commands/qring-setup-project.md | 0 .../commands/qring-teleport-secrets.md | 0 claude-code-plugin/hooks/hooks.json | 36 +++ .../scripts/qring-post-edit-lint-hint.sh | 0 .../hooks/scripts/qring-pre-bash-env-guard.sh | 0 .../hooks/scripts/qring-session-start.sh | 0 .../skills/exec-with-secrets/SKILL.md | 11 +- .../skills/project-onboarding/SKILL.md | 10 +- .../skills/secret-management/SKILL.md | 6 +- .../skills/secret-rotation/SKILL.md | 8 +- .../skills/secret-scanning/SKILL.md | 4 +- .../skills/exec-with-secrets/SKILL.md | 29 +++ docs/cli-mcp-parity.md | 26 ++- docs/quickstart-claude-code.md | 51 +++++ docs/quickstart-cursor.md | 41 ++++ docs/quickstart-kiro.md | 43 ++++ docs/releasing.md | 41 ++++ docs/troubleshooting.md | 82 +++++++ .../steering/qring-exec-with-secrets.md | 11 +- .../steering/qring-project-onboarding.md | 10 +- .../steering/qring-secret-management.md | 6 +- kiro-plugin/steering/qring-secret-rotation.md | 8 +- kiro-plugin/steering/qring-secret-scanning.md | 4 +- package.json | 3 + scripts/check-plugin-parity.mjs | 123 ++++++++++ scripts/plugin-generate-skills.mjs | 96 ++++++++ scripts/plugin-sync-claude.mjs | 21 +- scripts/prepare-release.mjs | 91 ++++++++ scripts/sync-versions.mjs | 14 +- src/__tests__/core/approval.test.ts | 7 + src/__tests__/core/dashboard-server.test.ts | 68 ++++++ src/cli/commands.ts | 7 +- src/cli/commands/agent.ts | 74 +++++- src/cli/commands/audit.ts | 76 +++++-- src/cli/commands/completion.ts | 151 ++++++++++++ src/cli/commands/doctor.ts | 215 ++++++++++++++++++ src/cli/commands/hooks.ts | 5 +- src/cli/commands/project.ts | 124 +++++++--- src/cli/commands/quantum.ts | 72 ++++-- src/cli/commands/secrets.ts | 105 ++++++++- src/cli/commands/security.ts | 6 +- src/cli/commands/tooling.ts | 31 ++- src/cli/commands/validation.ts | 60 ++++- src/cli/helpers.ts | 10 + src/core/dashboard-client.ts | 2 +- src/core/dashboard-html.ts | 9 + src/core/dashboard.ts | 40 +++- src/dashboard-client/main.js | 55 ++++- src/index.ts | 16 +- src/utils/prompt.ts | 50 +++- 71 files changed, 2232 insertions(+), 203 deletions(-) create mode 100644 .claude-plugin/marketplace.json create mode 100644 .github/workflows/release.yml create mode 100644 CONTRIBUTING.md create mode 100644 claude-code-plugin/.claude-plugin/plugin.json rename claude-code-plugin/{.claude => }/agents/secret-ops.md (100%) rename claude-code-plugin/{.claude => }/agents/security-auditor.md (100%) rename claude-code-plugin/{.claude => }/commands/qring-analyze.md (100%) rename claude-code-plugin/{.claude => }/commands/qring-dashboard.md (100%) rename claude-code-plugin/{.claude => }/commands/qring-exec-safe.md (100%) rename claude-code-plugin/{.claude => }/commands/qring-health-check.md (100%) rename claude-code-plugin/{.claude => }/commands/qring-rotate-expired.md (100%) rename claude-code-plugin/{.claude => }/commands/qring-scan-secrets.md (100%) rename claude-code-plugin/{.claude => }/commands/qring-setup-project.md (100%) rename claude-code-plugin/{.claude => }/commands/qring-teleport-secrets.md (100%) create mode 100644 claude-code-plugin/hooks/hooks.json rename claude-code-plugin/{.claude => }/hooks/scripts/qring-post-edit-lint-hint.sh (100%) rename claude-code-plugin/{.claude => }/hooks/scripts/qring-pre-bash-env-guard.sh (100%) rename claude-code-plugin/{.claude => }/hooks/scripts/qring-session-start.sh (100%) rename claude-code-plugin/{.claude => }/skills/exec-with-secrets/SKILL.md (55%) rename claude-code-plugin/{.claude => }/skills/project-onboarding/SKILL.md (90%) rename claude-code-plugin/{.claude => }/skills/secret-management/SKILL.md (94%) rename claude-code-plugin/{.claude => }/skills/secret-rotation/SKILL.md (91%) rename claude-code-plugin/{.claude => }/skills/secret-scanning/SKILL.md (98%) create mode 100644 cursor-plugin/skills/exec-with-secrets/SKILL.md create mode 100644 docs/quickstart-claude-code.md create mode 100644 docs/quickstart-cursor.md create mode 100644 docs/quickstart-kiro.md create mode 100644 docs/releasing.md create mode 100644 docs/troubleshooting.md create mode 100644 scripts/check-plugin-parity.mjs create mode 100644 scripts/plugin-generate-skills.mjs create mode 100644 scripts/prepare-release.mjs create mode 100644 src/__tests__/core/dashboard-server.test.ts create mode 100644 src/cli/commands/completion.ts create mode 100644 src/cli/commands/doctor.ts diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..2c1ff62 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,32 @@ +{ + "name": "q-ring", + "owner": { + "name": "I4cTime", + "url": "https://github.com/I4cTime" + }, + "metadata": { + "description": "Quantum keyring for AI coding tools — secrets, superposition, entanglement, MCP.", + "version": "0.12.0" + }, + "plugins": [ + { + "name": "qring", + "source": "./claude-code-plugin", + "description": "Quantum keyring for AI agents — manage secrets, scan for leaks, rotate keys, and enforce policy directly from Claude Code.", + "version": "0.12.0", + "author": { + "name": "I4cTime" + }, + "homepage": "https://qring.i4c.studio", + "license": "AGPL-3.0", + "keywords": [ + "secrets", + "keyring", + "security", + "mcp", + "api-keys" + ], + "category": "security" + } + ] +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 149b8c8..5dda8b0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -14,7 +14,7 @@ body: attributes: label: q-ring version description: "Run `qring --version` to find this." - placeholder: "0.2.9" + placeholder: "0.12.0" validations: required: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 583109e..d816669 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,9 +31,6 @@ jobs: - run: pnpm install --frozen-lockfile - - name: Security audit - run: pnpm audit --audit-level=high - - name: Typecheck run: pnpm run typecheck @@ -45,3 +42,117 @@ jobs: - name: Test run: pnpm run test:ci + + # Exercise the actual published entry points so a broken shebang, + # bundling regression, or bad bin mapping fails CI instead of a release. + - name: Smoke test built CLI + run: | + VER=$(node -p "require('./package.json').version") + OUT=$(node dist/index.js --version) + if [ "$OUT" != "$VER" ]; then + echo "::error::qring --version printed '$OUT', expected '$VER'" + exit 1 + fi + node dist/index.js --help > /dev/null + + - name: Smoke test MCP server (initialize handshake) + run: | + REQ='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci-smoke","version":"0.0.0"}}}' + RESP=$( (printf '%s\n' "$REQ" | timeout 15 node dist/mcp.js | head -n 1) || true ) + if ! printf '%s' "$RESP" | grep -q '"serverInfo"'; then + echo "::error::qring-mcp did not answer the initialize request" + printf 'got: %s\n' "$RESP" + exit 1 + fi + + # Plugins must stay in lockstep: same commands/agents/skills across + # Cursor/Claude/Kiro, doc'd tool counts matching the code, versions stamped, + # and generated artifacts (skills, dashboard bundle) freshly regenerated. + parity: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Plugin parity + generated skills up to date + run: pnpm run check:parity + + - name: Dashboard client bundle is fresh + run: | + pnpm run build:dashboard + git diff --exit-code src/core/dashboard-client.ts + + # Cross-platform / node-range coverage. The keyring backend differs per OS + # (Secret Service / macOS Keychain / Windows Credential Manager), and + # engines declares >=18 while the primary job only exercises ubuntu+22. + # Experimental (continue-on-error) until proven green; then make blocking. + compat: + name: compat (${{ matrix.os }}, node ${{ matrix.node }}) + runs-on: ${{ matrix.os }} + continue-on-error: true + strategy: + fail-fast: false + matrix: + include: + # engines floor — build + smoke only (vitest 4 requires Node 20+) + - { os: ubuntu-latest, node: 18, tests: false } + - { os: ubuntu-latest, node: 24, tests: true } + - { os: macos-latest, node: 22, tests: true } + - { os: windows-latest, node: 22, tests: true } + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm run build + + - name: Test + if: matrix.tests + run: pnpm run test:ci + + - name: Smoke test built CLI + shell: bash + run: | + VER=$(node -p "require('./package.json').version") + OUT=$(node dist/index.js --version) + if [ "$OUT" != "$VER" ]; then + echo "::error::qring --version printed '$OUT', expected '$VER'" + exit 1 + fi + node dist/index.js --help > /dev/null + + # Advisory-only: a freshly disclosed CVE in a transitive dep should not + # block unrelated PRs. Dependabot handles the actual upgrades. + audit: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Security audit (non-blocking) + run: pnpm audit --audit-level=high diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9c82691..6f52972 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,7 +2,7 @@ name: CodeQL on: push: - branches: [develop] + branches: [develop, main] pull_request: branches: [develop, main] schedule: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e2d67d3..01aa6f5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,7 +2,9 @@ name: Publish to npm & MCP Registry on: release: - types: [created] + # "published" (not "created") so draft releases never trigger a publish; + # matches the trigger update-homebrew.yml already uses. + types: [published] # Allow re-publishing a tag manually if the release-triggered run fails. # Pick the tag (or any branch) when dispatching from the Actions tab. workflow_dispatch: @@ -11,11 +13,25 @@ on: description: "Git ref to publish (defaults to the current branch)" required: false type: string + # Called from release.yml on tag push (GITHUB_TOKEN-created releases don't + # emit events, so the release:published trigger can't fire for that path). + workflow_call: + inputs: + ref: + description: "Git ref to publish (defaults to the calling ref)" + required: false + type: string permissions: contents: read id-token: write +# Never run two publishes at once (e.g. release event + manual re-dispatch); +# don't cancel an in-flight publish either. +concurrency: + group: publish + cancel-in-progress: false + jobs: publish: runs-on: ubuntu-latest @@ -24,6 +40,11 @@ jobs: with: ref: ${{ inputs.ref || github.ref }} + # pnpm version comes from the "packageManager" field in package.json; + # do not also pin "version" here (pnpm/action-setup errors on conflict). + # Must run before setup-node so `cache: pnpm` can find the pnpm binary. + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 with: # Node 24 bundles npm 11.5+, which is the minimum required for @@ -35,6 +56,7 @@ jobs: # https://docs.npmjs.com/trusted-publishers node-version: "24" registry-url: "https://registry.npmjs.org" + cache: pnpm # Intentionally NOT running `npm install -g npm@latest` here: # on the runner image it has been observed to fail with @@ -42,10 +64,6 @@ jobs: # self-upgrade. Node 24's bundled npm is already >= 11.5.1, so # no upgrade is needed for trusted publishing or provenance. - # pnpm version comes from the "packageManager" field in package.json; - # do not also pin "version" here (pnpm/action-setup errors on conflict). - - uses: pnpm/action-setup@v4 - - run: pnpm install --frozen-lockfile - name: Typecheck @@ -65,8 +83,12 @@ jobs: fi - name: Install mcp-publisher + # Pinned (not `latest`) so a breaking mcp-publisher release or renamed + # asset can't silently break the publish critical path. + env: + MCP_PUBLISHER_VERSION: v1.7.9 run: | - curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" | tar xz mcp-publisher + curl -fsSL "https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/mcp-publisher_linux_amd64.tar.gz" | tar xz mcp-publisher - name: Authenticate to MCP Registry run: ./mcp-publisher login github-oidc diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..39b88db --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,71 @@ +name: Release + +# Pushing a v* tag drives the whole release: create the GitHub Release with +# notes from the matching CHANGELOG section, then chain the npm/MCP publish +# and the Homebrew tap update via workflow_call. +# +# Why the explicit chaining: releases created here use GITHUB_TOKEN, and +# GITHUB_TOKEN-created events do not trigger other workflows, so the +# release:published triggers on publish.yml / update-homebrew.yml would never +# fire for tag-driven releases. Those event triggers still work for releases +# created manually in the UI; every downstream step is idempotent, so the +# occasional double-run is harmless. + +on: + push: + tags: ["v*"] + +permissions: + contents: write + id-token: write + +concurrency: + group: publish + cancel-in-progress: false + +jobs: + github-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Extract release notes from CHANGELOG + run: | + VERSION="${GITHUB_REF_NAME#v}" + # Section between "## [x.y.z]" and the next "## [" heading. + awk -v ver="$VERSION" ' + $0 ~ "^## \\[" ver "\\]" { grab = 1; next } + grab && /^## \[/ { exit } + grab { print } + ' CHANGELOG.md > /tmp/notes.md + if ! [ -s /tmp/notes.md ]; then + echo "::error::CHANGELOG.md has no [$VERSION] section — run scripts/prepare-release.mjs before tagging" + exit 1 + fi + + - name: Create GitHub release (idempotent) + env: + GH_TOKEN: ${{ github.token }} + run: | + if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then + echo "Release $GITHUB_REF_NAME already exists — skipping creation." + else + gh release create "$GITHUB_REF_NAME" \ + --repo "$GITHUB_REPOSITORY" \ + --title "$GITHUB_REF_NAME" \ + --notes-file /tmp/notes.md + fi + + publish: + needs: github-release + uses: ./.github/workflows/publish.yml + permissions: + contents: read + id-token: write + + homebrew: + needs: publish + uses: ./.github/workflows/update-homebrew.yml + secrets: inherit + permissions: + contents: read diff --git a/.github/workflows/update-homebrew.yml b/.github/workflows/update-homebrew.yml index cc81f48..3b9d945 100644 --- a/.github/workflows/update-homebrew.yml +++ b/.github/workflows/update-homebrew.yml @@ -3,10 +3,18 @@ name: Update Homebrew Tap on: release: types: [published] + # Called from release.yml on tag push (GITHUB_TOKEN-created releases don't + # emit events). GITHUB_REF_NAME is the tag in both paths. + workflow_call: permissions: contents: read +# One formula update at a time; never cancel one mid-push. +concurrency: + group: update-homebrew + cancel-in-progress: false + jobs: update-formula: runs-on: ubuntu-latest @@ -35,9 +43,16 @@ jobs: - name: Compute tarball sha256 id: sha run: | + set -euo pipefail VERSION="${{ steps.version.outputs.version }}" URL="https://registry.npmjs.org/@i4ctime/q-ring/-/q-ring-${VERSION}.tgz" - SHA=$(curl -sL "$URL" | sha256sum | awk '{print $1}') + # -f makes a 404 fail the download instead of hashing an error page. + curl -fsSL "$URL" -o /tmp/qring.tgz + SHA=$(sha256sum /tmp/qring.tgz | awk '{print $1}') + if [ -z "$SHA" ]; then + echo "::error::Failed to compute sha256 for ${URL}" + exit 1 + fi echo "sha256=${SHA}" >> "$GITHUB_OUTPUT" echo "url=${URL}" >> "$GITHUB_OUTPUT" echo "SHA256: ${SHA}" @@ -68,7 +83,8 @@ jobs: end test do - assert_match "qring", shell_output("#{bin}/qring --version") + # \`qring --version\` prints the bare version (Commander default). + assert_match "${VERSION}", shell_output("#{bin}/qring --version") end end RUBY @@ -80,5 +96,9 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add Formula/qring.rb - git commit -m "Update qring to ${VERSION}" - git push + if git diff --cached --quiet; then + echo "Formula already up to date for ${VERSION} — nothing to push." + else + git commit -m "Update qring to ${VERSION}" + git push + fi diff --git a/.gitignore b/.gitignore index 59fa3fc..8febd25 100644 --- a/.gitignore +++ b/.gitignore @@ -28,18 +28,20 @@ package-lock.json !src/__tests__/**/*credential* !cursor-plugin/skills/secret-*/ !cursor-plugin/skills/secret-*/** +!cursor-plugin/skills/exec-with-secrets/ +!cursor-plugin/skills/exec-with-secrets/** !cursor-plugin/rules/secret-*.mdc !cursor-plugin/agents/secret-*.md !cursor-plugin/commands/*secret*.md !cursor-plugin/commands/*token*.md !.cursor/rules/secret-*.mdc -!claude-code-plugin/.claude/skills/secret-*/ -!claude-code-plugin/.claude/skills/secret-*/** -!claude-code-plugin/.claude/skills/exec-with-secrets/ -!claude-code-plugin/.claude/skills/exec-with-secrets/** -!claude-code-plugin/.claude/agents/secret-*.md -!claude-code-plugin/.claude/commands/*secret*.md -!claude-code-plugin/.claude/commands/*token*.md +!claude-code-plugin/skills/secret-*/ +!claude-code-plugin/skills/secret-*/** +!claude-code-plugin/skills/exec-with-secrets/ +!claude-code-plugin/skills/exec-with-secrets/** +!claude-code-plugin/agents/secret-*.md +!claude-code-plugin/commands/*secret*.md +!claude-code-plugin/commands/*token*.md !kiro-plugin/steering/qring-secret-*.md !kiro-plugin/steering/qring-*-secrets.md !kiro-plugin/steering/qring-exec-with-secrets.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a4fa4ff..2473659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,34 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- **`qring has `** — script-friendly existence check (exit 0 if present, 1 if not; `--quiet` for exit-code-only, honors `--json`). Decay-aware like the MCP `has_secret` tool, closing the last MCP→CLI parity gap. +- **Claude Code plugin is now a real installable plugin** — added `claude-code-plugin/.claude-plugin/plugin.json` and a repo-root `.claude-plugin/marketplace.json`, so it installs via `/plugin marketplace add I4cTime/quantum_ring` + `/plugin install qring@q-ring` instead of file copying. Plugin components (agents/commands/skills/hooks) moved from `claude-code-plugin/.claude/` to the plugin root to match the Claude Code plugin format; hooks are wired via `hooks/hooks.json` (`${CLAUDE_PLUGIN_ROOT}`) for plugin installs and `.claude/settings.json` for project-scoped sync installs. `sync-versions` now stamps both new manifests. +- **`CONTRIBUTING.md`** — dev environment, branch/commit conventions, and the list of files to keep in sync (parity doc, plugin manifests, dashboard client bundle). +- **Docker install instructions** in the README for the MCP-server container path. +- **CI smoke tests** — the built `qring` binary (`--version`, `--help`) and an MCP `initialize` handshake against `dist/mcp.js` now run in CI after the build. +- **`--json` is now honored by ~20 more commands** — `list`, `inspect`, `delete`, `import`, `audit`, `audit:verify`, `health`, `analyze`, `recall`, `check`, `env`, `validate` (single/`--all`/`--list-providers`), `rotate`, `tunnel list`, `hook list`, `approvals`, `scan`, `lint` all emit `{ ok: true, data: … }`, making the whole read surface scriptable. Previously only 4 commands supported the advertised global flag. +- **Confirmation prompts on destructive operations** — `delete`, `forget --all`, `teleport unpack` (overwriting existing keys), `wizard` (regenerating existing keys), and `env:generate -o` (existing file) now ask before proceeding. Pass `-y`/`--yes` to skip; non-interactive use without `--yes` errors instead of silently proceeding. **Breaking for scripts that relied on unconfirmed `qring delete`/`forget --all` — add `--yes`.** +- **Plugin skills are now generated from a single source** — `cursor-plugin/skills/*/SKILL.md` is canonical; `scripts/plugin-generate-skills.mjs` derives the Claude Code skills and Kiro steering files (`pnpm run plugin:gen-skills`). Reconciles the cosmetic drift that hand-copying had accumulated. +- **CI parity guard** (`pnpm run check:parity` + a `parity` CI job) — fails when the three plugins' command/agent/skill sets diverge, doc'd MCP tool counts don't match the registered count (44), manifest versions drift from `package.json`, generated skills are stale, or the committed dashboard client bundle differs from a fresh build. +- **Cross-platform CI matrix** (experimental, non-blocking) — macOS + Windows on Node 22, ubuntu on Node 24, and a Node 18 engines-floor leg (build + CLI smoke; vitest 4 needs Node 20+). Exercises the per-OS keyring backends for the first time. +- **`qring doctor`** — one-shot install diagnosis: keyring-backend round-trip probe (the headless-Linux failure mode), audit-dir writability + hash-chain integrity, `.q-ring.json` validity, active policy, and `qring-mcp` on `PATH`. Supports `--json`; exits 1 on any failed check. +- **`qring completion bash|zsh|fish`** — shell completion scripts generated from the live command tree, so they can never drift from the actual commands/flags. +- **Dashboard: entanglement graph** — entangled secrets now render as an SVG node/edge graph (circular layout, up to 14 keys; falls back to the pair list beyond that), as the docs always advertised. +- **Dashboard: audit-chain badge** — the Audit Log card shows hash-chain integrity live (same check as `qring audit:verify`), green when intact, red with the break position when tampered. +- **Tag-driven release automation** — `pnpm run release:prepare ` bumps the version, syncs all manifests, rolls the CHANGELOG, and runs the parity guard; pushing the `v*` tag then runs `release.yml`, which creates the GitHub Release (notes from the CHANGELOG) and chains the npm/MCP publish and Homebrew update via `workflow_call`. See `docs/releasing.md`. +- **Docs** — `docs/troubleshooting.md` (keyring backends, MCP connection, approval gate, policy-root pinning, scripting gotchas, AGPL FAQ), per-editor quickstarts (`docs/quickstart-{claude-code,cursor,kiro}.md`), `docs/releasing.md`, and a docs index in the README. + +### Changed +- **CLI errors are now one-line messages instead of stack traces** — a central handler catches errors thrown by any command (policy denials, keyring backend failures) and prints them in red; set `QRING_DEBUG=1` for the full stack. +- **`pnpm audit` moved off the blocking CI path** — it runs as a separate non-blocking job, so a freshly disclosed advisory in a transitive dependency no longer fails unrelated PRs. +- **npm publish now triggers on `release: published`** (was `created`), so draft releases can no longer publish; `mcp-publisher` is pinned to v1.7.9 instead of `latest`; publish and Homebrew workflows gained concurrency guards. + +### Fixed +- **`qring set ` no longer hangs when the value is piped** — `promptSecret` reads stdin to EOF when it isn't a TTY (e.g. `echo "$VALUE" | qring set KEY`). +- **Homebrew formula test** asserted the string `"qring"` in `qring --version` output, which prints only the bare version — `brew test` would have failed on every release. It now asserts the version string. +- **Status dashboard actually sends security headers** — `Content-Security-Policy` (default-deny, inline-only), `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer` (keeps the `?token=` URL out of Referer headers), and `Cache-Control: no-store` on every response. Previously the page was CSP-*compatible* but no policy was enforced. Added tests for token auth (403) and the new headers. + ## [0.12.0] — 2026-06-24 ### Security diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8c88111 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,54 @@ +# Contributing to q-ring + +Thanks for helping improve q-ring! This guide covers the dev environment, project conventions, and the release-adjacent files you must keep in sync. + +## Dev environment + +- **Node** ≥ 18 (CI runs 22, releases build on 24) and **pnpm** — the exact pnpm version is pinned via the `packageManager` field in `package.json` (use `corepack enable` and it resolves automatically). +- Install and build: + + ```bash + pnpm install + pnpm run build + ``` + +- Useful scripts: + + | Script | What it does | + |---|---| + | `pnpm run typecheck` | `tsc --noEmit` | + | `pnpm run lint` | ESLint over `src/`, zero warnings allowed | + | `pnpm test` | vitest in watch mode | + | `pnpm run test:ci` | single vitest run (what CI runs) | + | `pnpm run build` | bundles the dashboard client, then tsup → `dist/` | + | `pnpm run format` | prettier over `src/` and `scripts/` | + +- Tests and sandboxes can point the audit log elsewhere with `QRING_AUDIT_DIR` (directory is created if missing); default is `~/.config/q-ring/audit.jsonl`. +- `QRING_DEBUG=1` makes the CLI print full stack traces instead of one-line errors. +- Optional local pre-commit: `qring hook:install` (uses this package's `precommit` hook when `qring` is on your `PATH`). + +## Branches and commits + +- Base your work on **`develop`**; `main` tracks releases. +- Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `chore:`, with optional scope — e.g. `fix(ci): …`), matching the existing history and the Dependabot config. +- Before opening a PR, run `pnpm run lint`, `pnpm run typecheck`, and `pnpm run test:ci` — the PR template checklist mirrors CI. + +## Keep these in sync when you change behavior + +- **CLI ↔ MCP parity:** [docs/cli-mcp-parity.md](docs/cli-mcp-parity.md) maps every CLI command to its MCP tool. Update it when adding, renaming, or removing either surface. +- **Editor plugins:** after changing plugin content: + - **Skills are generated** — edit only `cursor-plugin/skills/*/SKILL.md`, then run `pnpm run plugin:gen-skills` to regenerate the Claude Code skills and Kiro steering files, and commit the output. CI (`pnpm run check:parity`) fails if they drift, if the plugins' command/agent/skill sets diverge, if doc'd MCP tool counts don't match the code, or if manifest versions are stale. + - Cursor: `pnpm run plugin:sync` copies `cursor-plugin/` to `~/.cursor/plugins/local/my-plugin`. + - Kiro: `pnpm run plugin:sync:kiro` copies `kiro-plugin/mcp.json`, `steering/`, and `hooks/` into `~/.kiro`. + - Claude Code: `pnpm run plugin:sync:claude` copies the plugin into the current project (`--user` for `~/.claude`). +- **Versions:** `pnpm run sync-versions` stamps `package.json`'s version into the Cursor and Claude Code plugin manifests, `server.json`, and `SECURITY.md`. It runs automatically on `prepublishOnly`. +- **Dashboard client:** `src/core/dashboard-client.ts` is generated from `src/dashboard-client/main.js` — run `pnpm run build:dashboard` after editing the client and commit the regenerated file. +- **CHANGELOG:** add your change under `[Unreleased]` in `CHANGELOG.md` (Keep a Changelog format). + +## Security issues + +Do **not** open a public issue for vulnerabilities — follow [SECURITY.md](SECURITY.md). + +## License + +By contributing you agree your work is licensed under [AGPL-3.0-only](LICENSE), the same license as the project. diff --git a/README.md b/README.md index 6113112..a7378e2 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,19 @@ yarn global add @i4ctime/q-ring brew install i4ctime/tap/qring ``` +### Docker (MCP server) + +The repo ships a `Dockerfile` that builds the MCP server and exposes it through [`mcp-proxy`](https://github.com/punkpeye/mcp-proxy) — useful for hosted MCP deployments (e.g. Glama) or keeping the server off the host entirely: + +```bash +git clone https://github.com/I4cTime/quantum_ring.git +cd quantum_ring +docker build -t qring-mcp . +docker run --rm -p 8080:8080 qring-mcp +``` + +> Note: inside a container there is no OS keychain (GNOME Keyring / macOS Keychain), so this path is for the MCP protocol surface, ephemeral use, and CI experiments — not for durable local secret storage. For day-to-day use install the CLI natively via one of the package managers above. + ## ⚡ Quick Start ```bash @@ -61,6 +74,12 @@ qring generate --format api-key --prefix "sk-" --save MY_KEY # 5️⃣ Run a full health scan qring health + +# Something not working? Diagnose the install (keyring, audit, MCP wiring) +qring doctor + +# Tab completion for your shell +qring completion zsh > ~/.zsh/completions/_qring # also: bash, fish ``` ## Quantum Features @@ -241,6 +260,9 @@ qring list --stale # Glob pattern on key name qring list --filter "API_*" + +# Script-friendly existence check (exit 0 if present, 1 if not; decay-aware) +qring has OPENAI_API_KEY --quiet && echo "configured" ``` ### Project Secret Manifest @@ -944,8 +966,17 @@ Optional per-project configuration: - **`validationUrl`** configures the generic HTTP provider's endpoint for custom validation - **`policy`** defines governance rules for MCP tool gating, key access restrictions, exec allowlists, and secret lifecycle requirements +## 📚 Docs + +- [Quickstart: Claude Code](docs/quickstart-claude-code.md) · [Cursor](docs/quickstart-cursor.md) · [Kiro](docs/quickstart-kiro.md) +- [Troubleshooting](docs/troubleshooting.md) — keyring backends, MCP connection, approval gate, policy pinning +- [CLI ↔ MCP parity](docs/cli-mcp-parity.md) — every command mapped to its MCP tool +- [Releasing](docs/releasing.md) — tag-driven release flow + ## Contributing +See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide (dev environment, conventions, files to keep in sync). The short version: + - Run **`pnpm run lint`**, **`pnpm run typecheck`**, and **`pnpm run test:ci`** before opening a PR. - Tests or sandboxes can point the audit log elsewhere with **`QRING_AUDIT_DIR`** (directory is created if missing); default is `~/.config/q-ring/audit.jsonl`. - Optional local pre-commit: **`qring hook:install`** (uses this package’s `precommit` hook when `qring` is on your `PATH`). diff --git a/claude-code-plugin/.claude-plugin/plugin.json b/claude-code-plugin/.claude-plugin/plugin.json new file mode 100644 index 0000000..98571cb --- /dev/null +++ b/claude-code-plugin/.claude-plugin/plugin.json @@ -0,0 +1,23 @@ +{ + "name": "qring", + "description": "Quantum keyring for AI agents — manage secrets, scan for leaks, rotate keys, and enforce policy directly from Claude Code.", + "version": "0.12.0", + "author": { + "name": "I4cTime" + }, + "homepage": "https://qring.i4c.studio", + "repository": "https://github.com/I4cTime/quantum_ring", + "license": "AGPL-3.0", + "keywords": [ + "secrets", + "keyring", + "security", + "mcp", + "api-keys", + "dotenv", + "credential-management", + "secret-scanning", + "secret-rotation", + "governance" + ] +} diff --git a/claude-code-plugin/README.md b/claude-code-plugin/README.md index 64064e0..1c7e3ec 100644 --- a/claude-code-plugin/README.md +++ b/claude-code-plugin/README.md @@ -35,7 +35,7 @@ Loaded into every Claude Code conversation in this project. Provides the always- Project-scoped MCP config that connects Claude Code to the local `qring-mcp` binary over stdio. All 44 q-ring MCP tools become available in chat. -### Subagents (`.claude/agents/*.md`) +### Subagents (`agents/*.md`) | Subagent | Purpose | |----------|---------| @@ -44,7 +44,7 @@ Project-scoped MCP config that connects Claude Code to the local `qring-mcp` bin Invoke explicitly with `> Use the secret-ops subagent to store my OpenAI key` or let Claude Code delegate automatically based on the description. -### Slash commands (`.claude/commands/*.md`) +### Slash commands (`commands/*.md`) | Command | Action | |---------|--------| @@ -57,7 +57,7 @@ Invoke explicitly with `> Use the secret-ops subagent to store my OpenAI key` or | `/qring-exec-safe` | Run a command with secrets injected and stdout/stderr redacted | | `/qring-analyze` | Usage analytics and optimization suggestions | -### Skills (`.claude/skills/*/SKILL.md`) +### Skills (`skills/*/SKILL.md`) | Skill | Triggers On | |-------|-------------| @@ -69,7 +69,7 @@ Invoke explicitly with `> Use the secret-ops subagent to store my OpenAI key` or Skills are progressively disclosed — Claude Code reads only the metadata until a skill is relevant, then loads the full body. -### Hooks (`.claude/settings.json`) +### Hooks (`hooks/hooks.json`, or `.claude/settings.json` for project installs) | Event | Behavior | |-------|----------| @@ -77,11 +77,22 @@ Skills are progressively disclosed — Claude Code reads only the metadata until | `PreToolUse` (`Bash`) | Block commands that look like they would commit / print `.env` files; warn about secret leakage | | `SessionStart` | Print a one-line reminder to call `get_project_context` if a `.q-ring.json` is detected | -> Hook scripts live in `hooks/scripts/`. They are pure Bash with no runtime dependencies — safe to read before installing. +> Hook scripts live in `hooks/scripts/`. They are pure Bash with no runtime dependencies — safe to read before installing. Plugin installs wire them via `hooks/hooks.json` (`${CLAUDE_PLUGIN_ROOT}` paths); the project-scoped sync wires them via `.claude/settings.json` instead. ## Installation -### One-shot sync (recommended) +### Plugin install (recommended) + +Install through Claude Code's plugin system — no file copying, updates ship with the marketplace: + +``` +/plugin marketplace add I4cTime/quantum_ring +/plugin install qring@q-ring +``` + +This registers the MCP server, slash commands, subagents, skills, and hooks in one step. You still need the `@i4ctime/q-ring` package installed globally (see Prerequisites) so `qring-mcp` is on `PATH`. + +### One-shot project sync From the `quantum_ring` repo root: @@ -94,7 +105,8 @@ This copies into your project (`$PWD` by default): - `claude-code-plugin/CLAUDE.md` → `./CLAUDE.md` - `claude-code-plugin/.mcp.json` → `./.mcp.json` -- `claude-code-plugin/.claude/` → `./.claude/` +- `claude-code-plugin/agents|commands|skills|hooks/` → `./.claude/…` +- `claude-code-plugin/.claude/settings.json` → `./.claude/settings.json` Pass a custom destination as the first argument: @@ -108,9 +120,9 @@ To install the agents, commands, skills, and hooks for **all** projects (instead ```bash mkdir -p ~/.claude -cp -r claude-code-plugin/.claude/agents ~/.claude/agents -cp -r claude-code-plugin/.claude/commands ~/.claude/commands -cp -r claude-code-plugin/.claude/skills ~/.claude/skills +cp -r claude-code-plugin/agents ~/.claude/agents +cp -r claude-code-plugin/commands ~/.claude/commands +cp -r claude-code-plugin/skills ~/.claude/skills cp claude-code-plugin/.claude/settings.json ~/.claude/settings.json ``` @@ -125,7 +137,12 @@ claude mcp add q-ring -- qring-mcp ### Manual project install ```bash -cp -r claude-code-plugin/.claude . +pnpm run plugin:sync:claude # preferred — handles the layout mapping for you +# or by hand: +mkdir -p .claude +cp -r claude-code-plugin/agents claude-code-plugin/commands claude-code-plugin/skills claude-code-plugin/hooks .claude/ +rm -f .claude/hooks/hooks.json # plugin-only manifest; project installs use settings.json +cp claude-code-plugin/.claude/settings.json .claude/settings.json cp claude-code-plugin/CLAUDE.md CLAUDE.md cp claude-code-plugin/.mcp.json .mcp.json ``` diff --git a/claude-code-plugin/.claude/agents/secret-ops.md b/claude-code-plugin/agents/secret-ops.md similarity index 100% rename from claude-code-plugin/.claude/agents/secret-ops.md rename to claude-code-plugin/agents/secret-ops.md diff --git a/claude-code-plugin/.claude/agents/security-auditor.md b/claude-code-plugin/agents/security-auditor.md similarity index 100% rename from claude-code-plugin/.claude/agents/security-auditor.md rename to claude-code-plugin/agents/security-auditor.md diff --git a/claude-code-plugin/.claude/commands/qring-analyze.md b/claude-code-plugin/commands/qring-analyze.md similarity index 100% rename from claude-code-plugin/.claude/commands/qring-analyze.md rename to claude-code-plugin/commands/qring-analyze.md diff --git a/claude-code-plugin/.claude/commands/qring-dashboard.md b/claude-code-plugin/commands/qring-dashboard.md similarity index 100% rename from claude-code-plugin/.claude/commands/qring-dashboard.md rename to claude-code-plugin/commands/qring-dashboard.md diff --git a/claude-code-plugin/.claude/commands/qring-exec-safe.md b/claude-code-plugin/commands/qring-exec-safe.md similarity index 100% rename from claude-code-plugin/.claude/commands/qring-exec-safe.md rename to claude-code-plugin/commands/qring-exec-safe.md diff --git a/claude-code-plugin/.claude/commands/qring-health-check.md b/claude-code-plugin/commands/qring-health-check.md similarity index 100% rename from claude-code-plugin/.claude/commands/qring-health-check.md rename to claude-code-plugin/commands/qring-health-check.md diff --git a/claude-code-plugin/.claude/commands/qring-rotate-expired.md b/claude-code-plugin/commands/qring-rotate-expired.md similarity index 100% rename from claude-code-plugin/.claude/commands/qring-rotate-expired.md rename to claude-code-plugin/commands/qring-rotate-expired.md diff --git a/claude-code-plugin/.claude/commands/qring-scan-secrets.md b/claude-code-plugin/commands/qring-scan-secrets.md similarity index 100% rename from claude-code-plugin/.claude/commands/qring-scan-secrets.md rename to claude-code-plugin/commands/qring-scan-secrets.md diff --git a/claude-code-plugin/.claude/commands/qring-setup-project.md b/claude-code-plugin/commands/qring-setup-project.md similarity index 100% rename from claude-code-plugin/.claude/commands/qring-setup-project.md rename to claude-code-plugin/commands/qring-setup-project.md diff --git a/claude-code-plugin/.claude/commands/qring-teleport-secrets.md b/claude-code-plugin/commands/qring-teleport-secrets.md similarity index 100% rename from claude-code-plugin/.claude/commands/qring-teleport-secrets.md rename to claude-code-plugin/commands/qring-teleport-secrets.md diff --git a/claude-code-plugin/hooks/hooks.json b/claude-code-plugin/hooks/hooks.json new file mode 100644 index 0000000..d54024c --- /dev/null +++ b/claude-code-plugin/hooks/hooks.json @@ -0,0 +1,36 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/qring-post-edit-lint-hint.sh" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/qring-pre-bash-env-guard.sh" + } + ] + } + ], + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/qring-session-start.sh" + } + ] + } + ] + } +} diff --git a/claude-code-plugin/.claude/hooks/scripts/qring-post-edit-lint-hint.sh b/claude-code-plugin/hooks/scripts/qring-post-edit-lint-hint.sh similarity index 100% rename from claude-code-plugin/.claude/hooks/scripts/qring-post-edit-lint-hint.sh rename to claude-code-plugin/hooks/scripts/qring-post-edit-lint-hint.sh diff --git a/claude-code-plugin/.claude/hooks/scripts/qring-pre-bash-env-guard.sh b/claude-code-plugin/hooks/scripts/qring-pre-bash-env-guard.sh similarity index 100% rename from claude-code-plugin/.claude/hooks/scripts/qring-pre-bash-env-guard.sh rename to claude-code-plugin/hooks/scripts/qring-pre-bash-env-guard.sh diff --git a/claude-code-plugin/.claude/hooks/scripts/qring-session-start.sh b/claude-code-plugin/hooks/scripts/qring-session-start.sh similarity index 100% rename from claude-code-plugin/.claude/hooks/scripts/qring-session-start.sh rename to claude-code-plugin/hooks/scripts/qring-session-start.sh diff --git a/claude-code-plugin/.claude/skills/exec-with-secrets/SKILL.md b/claude-code-plugin/skills/exec-with-secrets/SKILL.md similarity index 55% rename from claude-code-plugin/.claude/skills/exec-with-secrets/SKILL.md rename to claude-code-plugin/skills/exec-with-secrets/SKILL.md index 5c107d8..8e6c014 100644 --- a/claude-code-plugin/.claude/skills/exec-with-secrets/SKILL.md +++ b/claude-code-plugin/skills/exec-with-secrets/SKILL.md @@ -3,7 +3,7 @@ name: exec-with-secrets description: Run shell commands with secrets injected from q-ring with stdout/stderr redaction and policy checks. Use when the user wants to run tests, migrations, or CLIs that need API keys without pasting secrets into the shell or .env. --- -# Exec with Secrets (MCP) +# Exec with secrets (MCP) ## When to use @@ -19,12 +19,11 @@ description: Run shell commands with secrets injected from q-ring with stdout/st ## Workflow -1. Call `get_policy_summary` if unsure whether `exec_with_secrets` is allowed (`policy.mcp` may deny this tool). -2. Prefer `check_policy` with `action: "exec"` when experimenting with deny/allow rules. -3. Call `exec_with_secrets` with the argv array, `projectPath`, and optional key filters so only required secrets are loaded. +1. Call **`get_policy_summary`** if unsure whether `exec_with_secrets` is allowed (`policy.mcp` may deny this tool). +2. Prefer **`check_policy`** with `action: "exec"` when experimenting with deny/allow rules. +3. Call **`exec_with_secrets`** with the argv array, `projectPath`, and optional key filters so only required secrets are loaded. ## Safety - Never log raw command output to public channels without reviewing redaction limits. -- Prefer `tunnel_create` / `tunnel_read` for one-off handoff of a single value to another agent when full `exec` is unnecessary. -- Use exec profiles (`restricted`, `ci`, `unrestricted`) to bound command behavior. +- Prefer **`tunnel_create` / `tunnel_read`** for one-off handoff of a single value to another agent when full `exec` is unnecessary. diff --git a/claude-code-plugin/.claude/skills/project-onboarding/SKILL.md b/claude-code-plugin/skills/project-onboarding/SKILL.md similarity index 90% rename from claude-code-plugin/.claude/skills/project-onboarding/SKILL.md rename to claude-code-plugin/skills/project-onboarding/SKILL.md index 0d8fc36..fc6b8bf 100644 --- a/claude-code-plugin/.claude/skills/project-onboarding/SKILL.md +++ b/claude-code-plugin/skills/project-onboarding/SKILL.md @@ -5,7 +5,7 @@ description: Set up q-ring for a new project — create manifests, detect enviro # Project Onboarding -## When to use +## When to Use Activate when the user: - Starts a new project and needs secret management @@ -20,7 +20,7 @@ Activate when the user: Call `detect_environment` to determine the current context. Sources checked in order: 1. Explicit `QRING_ENV` -2. `NODE_ENV` (mapped: `production` → `prod`, `development` → `dev`) +2. `NODE_ENV` (mapped: `production` -> `prod`, `development` -> `dev`) 3. Git branch + `.q-ring.json` `branchMap` (supports globs like `release/*`) 4. `.q-ring.json` `defaultEnv` @@ -58,7 +58,7 @@ Help the user create a `.q-ring.json` file with: If the project has `.env` files, offer to import them with `import_dotenv`. Use `skipExisting: true` to avoid overwriting. -### 5. Generate `.env` from manifest +### 5. Generate .env from manifest Call `env_generate` to produce a `.env` file from the manifest with all declared secrets resolved from q-ring. @@ -69,7 +69,7 @@ Call `register_hook` to set up notifications when secrets change: - HTTP hook: notify a deployment webhook - Signal hook: send SIGHUP to a running process -Use `list_hooks` to inspect the registry and `remove_hook` to delete entries by id. +Use **`list_hooks`** to inspect the registry and **`remove_hook`** to delete entries by id. (CLI-only: `qring hook enable|disable|test` for lifecycle control.) ### 7. Configure policy @@ -80,7 +80,7 @@ Guide the user through the `policy` section of `.q-ring.json`: Call `get_policy_summary` to verify the policy is loaded correctly. -## Best practices +## Best Practices - Always create a `.q-ring.json` manifest for team projects — it serves as documentation and validation - Use `branchMap` to automatically detect environments from git branches diff --git a/claude-code-plugin/.claude/skills/secret-management/SKILL.md b/claude-code-plugin/skills/secret-management/SKILL.md similarity index 94% rename from claude-code-plugin/.claude/skills/secret-management/SKILL.md rename to claude-code-plugin/skills/secret-management/SKILL.md index a180f60..5856a76 100644 --- a/claude-code-plugin/.claude/skills/secret-management/SKILL.md +++ b/claude-code-plugin/skills/secret-management/SKILL.md @@ -5,7 +5,7 @@ description: Manage secrets stored in q-ring — store, retrieve, list, inspect, # Secret Management -## When to use +## When to Use Activate when the user: - Asks to store, retrieve, delete, or list secrets @@ -54,9 +54,9 @@ Link secrets so rotating one updates the other: ### 6. Scopes -Secrets resolve through a cascade: project → team → org → global. Use the `scope` parameter to target a specific level. +Secrets resolve through a cascade: project -> team -> org -> global. Use the `scope` parameter to target a specific level. -## Best practices +## Best Practices - Always add a `description` when storing secrets for discoverability - Use `tags` to group related secrets (e.g., `database`, `api`, `auth`) diff --git a/claude-code-plugin/.claude/skills/secret-rotation/SKILL.md b/claude-code-plugin/skills/secret-rotation/SKILL.md similarity index 91% rename from claude-code-plugin/.claude/skills/secret-rotation/SKILL.md rename to claude-code-plugin/skills/secret-rotation/SKILL.md index 88c91e6..faff7af 100644 --- a/claude-code-plugin/.claude/skills/secret-rotation/SKILL.md +++ b/claude-code-plugin/skills/secret-rotation/SKILL.md @@ -5,7 +5,7 @@ description: Validate, rotate, and batch-check secrets against their providers. # Secret Rotation and Validation -## When to use +## When to Use Activate when the user: - Mentions expired, stale, or invalid secrets @@ -58,8 +58,8 @@ Call `list_providers` to see all supported validation providers with their prefi - **`status_dashboard`** — starts the local quantum status dashboard (browser UI on localhost). - **`agent_scan`** — one-shot health scan JSON (same family as CLI `qring agent --once`). -## Best practices +## Best Practices -- Store secrets with `rotationFormat` (e.g., `api-key`, `password`, `uuid`) to enable automatic rotation -- Set `provider` metadata for automatic validation detection +- Store secrets with `--rotation-format` (e.g., `api-key`, `password`, `uuid`) to enable automatic rotation +- Set `--provider` metadata for automatic validation detection - Run `ci_validate_secrets` in CI pipelines to catch expired credentials before deployment diff --git a/claude-code-plugin/.claude/skills/secret-scanning/SKILL.md b/claude-code-plugin/skills/secret-scanning/SKILL.md similarity index 98% rename from claude-code-plugin/.claude/skills/secret-scanning/SKILL.md rename to claude-code-plugin/skills/secret-scanning/SKILL.md index 4f3d21d..b716090 100644 --- a/claude-code-plugin/.claude/skills/secret-scanning/SKILL.md +++ b/claude-code-plugin/skills/secret-scanning/SKILL.md @@ -5,7 +5,7 @@ description: Scan and lint codebases for hardcoded secrets using entropy analysi # Secret Scanning -## When to use +## When to Use Activate when the user: - Asks to scan a project for hardcoded secrets or credentials @@ -46,7 +46,7 @@ Call `lint_files` with the list of affected files and `fix: true` to: After fixing, re-run `scan_codebase_for_secrets` to confirm no secrets remain. -## Best practices +## Best Practices - Run scanning before every PR or commit - Ignore placeholder values (`your_api_key`, `replace_me`, `example`) — the scanner handles this automatically diff --git a/cursor-plugin/skills/exec-with-secrets/SKILL.md b/cursor-plugin/skills/exec-with-secrets/SKILL.md new file mode 100644 index 0000000..8e6c014 --- /dev/null +++ b/cursor-plugin/skills/exec-with-secrets/SKILL.md @@ -0,0 +1,29 @@ +--- +name: exec-with-secrets +description: Run shell commands with secrets injected from q-ring with stdout/stderr redaction and policy checks. Use when the user wants to run tests, migrations, or CLIs that need API keys without pasting secrets into the shell or .env. +--- + +# Exec with secrets (MCP) + +## When to use + +- Running a project script that reads env vars you keep in q-ring +- One-off CLI tools (`curl`, SDK CLIs) that need a token from the keyring +- CI-like commands where redaction and `policy.exec` matter + +## Tool + +| Task | MCP tool | +|------|----------| +| Run command with env from q-ring | `exec_with_secrets` | + +## Workflow + +1. Call **`get_policy_summary`** if unsure whether `exec_with_secrets` is allowed (`policy.mcp` may deny this tool). +2. Prefer **`check_policy`** with `action: "exec"` when experimenting with deny/allow rules. +3. Call **`exec_with_secrets`** with the argv array, `projectPath`, and optional key filters so only required secrets are loaded. + +## Safety + +- Never log raw command output to public channels without reviewing redaction limits. +- Prefer **`tunnel_create` / `tunnel_read`** for one-off handoff of a single value to another agent when full `exec` is unnecessary. diff --git a/docs/cli-mcp-parity.md b/docs/cli-mcp-parity.md index 43a1207..edd51b9 100644 --- a/docs/cli-mcp-parity.md +++ b/docs/cli-mcp-parity.md @@ -32,7 +32,7 @@ approval gates. | Concern | CLI | MCP | | ------------------ | ------------------------------------------------ | --------------------------------------------------------------- | | Secret CRUD | `set` `get` `delete` `list` `inspect` `export` `import` | `set_secret` `get_secret` `delete_secret` `list_secrets` `inspect_secret` `export_secrets` `import_dotenv` | -| Existence check | `list` / `inspect` | `has_secret` | +| Existence check | `has` (exit 0/1, `--quiet` for scripts) | `has_secret` | | Generation | `generate` | `generate_secret` | | Entanglement | `entangle` / `disentangle` | `entangle_secrets` / `disentangle_secrets` | | Tunnel | `tunnel create/read/destroy/list` | `tunnel_create` / `tunnel_read` / `tunnel_destroy` / `tunnel_list` | @@ -64,20 +64,34 @@ approval gates. | `hook:install` / `hook:uninstall` / `hook:run` | Git pre-commit wiring | | `qring wizard` | Interactive service setup | | `hook enable` / `disable` / `test` | Runtime lifecycle for secret-change hooks | +| `qring doctor` | Install self-check (keyring, audit, manifest, MCP binary) | +| `qring completion ` | bash/zsh/fish completion scripts generated from the CLI itself | ## MCP-only (no CLI subcommand) | MCP tool | Notes | | --------------- | -------------------------------------------------------------------------------- | -| `has_secret` | Existence check — CLI users can use `qring inspect` or `qring list --filter`. | | `check_policy` | Programmatic policy probe — CLI surfaces the same information via `qring policy`. | ## JSON output -- **Global:** `qring --json …` applies to commands that support machine - output (see `wantsJsonOutput` in `src/cli/helpers.ts`). -- **Per-flag:** `qring context --json`, `qring ci:validate --json`, - `qring policy --json` remain valid aliases. +- **Global:** `qring --json …` (or `--json` on the subcommand) emits + `{ "ok": true, "data": … }` for: `get`, `has`, `delete`, `list`, `inspect`, + `import`, `audit`, `audit:verify`, `health`, `analyze`, `recall` (list + mode), `check`, `env`, `validate` (single, `--all`, `--list-providers`), + `rotate`, `tunnel list`, `hook list`, `approvals`, `scan`, `lint`. + See `emitJson` in `src/cli/helpers.ts`. +- **Legacy shapes:** `qring context --json`, `qring ci:validate --json`, and + `qring policy --json` print their payload without the `{ok, data}` envelope + (kept for backward compatibility). + +## Destructive-operation confirmations + +`delete`, `forget --all`, `teleport unpack` (when it would overwrite existing +keys), `wizard` (when its generated keys already exist), and +`env:generate -o` (when the output file exists) prompt for confirmation on a +TTY. Pass `-y`/`--yes` to skip; non-interactive use without `--yes` is an +error, never a silent yes. ## Intentional differences diff --git a/docs/quickstart-claude-code.md b/docs/quickstart-claude-code.md new file mode 100644 index 0000000..5cfd9da --- /dev/null +++ b/docs/quickstart-claude-code.md @@ -0,0 +1,51 @@ +# Quickstart: q-ring in Claude Code + +Five minutes from zero to an agent that manages your secrets safely. + +## 1. Install the CLI (once per machine) + +```bash +pnpm add -g @i4ctime/q-ring # or: npm i -g / brew install i4ctime/tap/qring +qring doctor # verify keyring backend + qring-mcp on PATH +``` + +## 2. Install the plugin + +Inside Claude Code: + +``` +/plugin marketplace add I4cTime/quantum_ring +/plugin install qring@q-ring +``` + +That registers the MCP server (all 44 tools), 8 slash commands, 2 subagents, 5 skills, and 3 safety hooks in one step. + +Prefer project-scoped files instead of a plugin? From a checkout of this repo: `pnpm run plugin:sync:claude -- /path/to/your/project`. + +## 3. First five minutes + +Store a key without pasting it into chat (the CLI prompts, masked): + +```bash +qring set OPENAI_API_KEY # value prompted, never in shell history or transcripts +``` + +Then drive everything from chat: + +| Try | What happens | +|---|---| +| `/qring-setup-project` | Wizard: manifest, env detection, `.env` import, hooks | +| `/qring-scan-secrets` | Scans the codebase for hardcoded secrets, offers auto-fix | +| "Run the test suite — it needs `OPENAI_API_KEY`" | Agent uses `exec_with_secrets`: key injected, output redacted | +| "Is my Stripe key still valid?" | `validate_secret` liveness-checks it against the provider | +| `/qring-dashboard` | Live localhost dashboard (secrets health, audit log, entanglement graph) | + +## 4. Guardrails you get automatically + +- The **SessionStart hook** points the agent at `get_project_context` when a `.q-ring.json` exists. +- The **PreToolUse hook** asks for confirmation before shell commands that would print or commit `.env` files. +- Secrets stored with `--requires-approval` refuse agent reads until you run `qring approve KEY --for 3600 --reason "…"`. + +## Troubleshooting + +MCP server not showing up, keyring errors, approval confusion → [troubleshooting.md](troubleshooting.md). Full plugin reference → [claude-code-plugin/README.md](../claude-code-plugin/README.md). diff --git a/docs/quickstart-cursor.md b/docs/quickstart-cursor.md new file mode 100644 index 0000000..6ee2f20 --- /dev/null +++ b/docs/quickstart-cursor.md @@ -0,0 +1,41 @@ +# Quickstart: q-ring in Cursor + +## 1. Install the CLI (once per machine) + +```bash +pnpm add -g @i4ctime/q-ring # or: npm i -g / brew install i4ctime/tap/qring +qring doctor # verify keyring backend + qring-mcp on PATH +``` + +## 2. Install the plugin + +From a checkout of this repo: + +```bash +pnpm install +pnpm run plugin:sync # copies cursor-plugin/ to ~/.cursor/plugins/local/my-plugin +``` + +Restart Cursor. You get the MCP server (all 44 tools), 3 always-on rules, 8 commands, 2 agents, 5 skills, and 3 natural-language hooks. + +## 3. First five minutes + +```bash +qring set OPENAI_API_KEY # value prompted, masked — never in chat or history +``` + +Then in Cursor chat: + +| Try | What happens | +|---|---| +| "Set up q-ring for this project" | Manifest, env detection, `.env` import, hooks | +| "Scan this repo for hardcoded secrets and fix them" | `scan_codebase_for_secrets` + auto-migration to q-ring | +| "Run the integration tests — they need `DATABASE_URL`" | `exec_with_secrets`: injected env, redacted output | +| "Which of my keys are expired or about to expire?" | `health_check` / decay report | +| "Show me the q-ring dashboard" | `status_dashboard` opens the live localhost view | + +The rules keep the agent from ever writing secrets into code or `.env` files; the skills trigger on secret-related requests automatically. + +## Troubleshooting + +MCP server not connecting, keyring errors → [troubleshooting.md](troubleshooting.md). Full plugin reference → [cursor-plugin/README.md](../cursor-plugin/README.md). diff --git a/docs/quickstart-kiro.md b/docs/quickstart-kiro.md new file mode 100644 index 0000000..00d3386 --- /dev/null +++ b/docs/quickstart-kiro.md @@ -0,0 +1,43 @@ +# Quickstart: q-ring in Kiro + +## 1. Install the CLI (once per machine) + +```bash +pnpm add -g @i4ctime/q-ring # or: npm i -g / brew install i4ctime/tap/qring +qring doctor # verify keyring backend + qring-mcp on PATH +``` + +## 2. Install the Power + +Preferred: add `kiro-plugin/` from a checkout of this repo as a **Power** via Kiro's Powers panel ([docs](https://kiro.dev/docs/powers/create/)) — it reads `POWER.md` and wires `mcp.json`, steering, and hooks. + +Or copy into your Kiro config directly: + +```bash +pnpm install +pnpm run plugin:sync:kiro # mcp.json → ~/.kiro/settings, plus steering/ and hooks/ +``` + +Kiro's `mcp.json` ships an `autoApprove` allowlist for the read-only tools, so status/context calls don't prompt. + +## 3. First five minutes + +```bash +qring set OPENAI_API_KEY # value prompted, masked +``` + +Steering docs activate on demand with `#` tags in chat: + +| Type | What you get | +|---|---| +| `#qring-secret-management` | Store/retrieve/organize workflow guidance + live tools | +| `#qring-secret-scanning` then "scan this repo" | Hardcoded-secret scan with auto-fix | +| `#qring-cmd-health-check` | Full health report — decay, anomalies, audit integrity | +| `#qring-exec-with-secrets` then "run the tests" | `exec_with_secrets` with redacted output | +| `#qring-cmd-dashboard` | Live localhost dashboard | + +The three hooks watch file writes and shell commands for `.env`/credential leaks. + +## Troubleshooting + +MCP server not connecting, keyring errors → [troubleshooting.md](troubleshooting.md). Full Power reference → [kiro-plugin/README.md](../kiro-plugin/README.md). diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..8f161f7 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,41 @@ +# Releasing q-ring + +Releases are tag-driven. One script stages everything; pushing the tag does the rest. + +## Flow + +```bash +# on develop, with a non-empty [Unreleased] section in CHANGELOG.md +pnpm run release:prepare minor # or patch / major / an explicit x.y.z +``` + +`scripts/prepare-release.mjs`: + +1. bumps `package.json`, +2. runs `sync-versions` (Cursor + Claude plugin manifests, marketplaces, `server.json`, `SECURITY.md`), +3. rolls the CHANGELOG's `[Unreleased]` into `[x.y.z] — YYYY-MM-DD` and leaves a fresh `[Unreleased]`, +4. runs the plugin-parity check, +5. prints the exact commit/tag/push commands. + +Review the diff, merge to `main` per the usual PR flow, then tag and push: + +```bash +git tag v0.13.0 +git push --tags +``` + +## What the tag triggers + +`.github/workflows/release.yml` (on `v*` tags): + +1. **GitHub Release** — created idempotently, with the release notes extracted from that version's CHANGELOG section. Tagging without a matching CHANGELOG section fails the run on purpose. +2. **Publish** (`publish.yml` via `workflow_call`) — npm publish with provenance (idempotent: skips if the version is already on npm), then MCP Registry publish via a pinned `mcp-publisher`. +3. **Homebrew** (`update-homebrew.yml` via `workflow_call`) — waits for the version to appear on npm, regenerates the tap formula with the new tarball sha, and pushes to `I4cTime/homebrew-tap` (skips if unchanged). + +Why `workflow_call` instead of the `release: published` event for this path: the workflow creates the release with `GITHUB_TOKEN`, and GitHub does not fire workflow triggers for events created by `GITHUB_TOKEN`. The event triggers remain in place so a release created manually in the GitHub UI still publishes; every downstream step is idempotent, so overlap is harmless (a shared `publish` concurrency group serializes runs). + +## Recovering from a failed release + +- **npm/MCP publish failed:** fix the cause, then re-run from the Actions tab with `workflow_dispatch` on `publish.yml`, passing the tag as `ref`. The npm step skips if the version already published. +- **Homebrew failed:** re-run the `update-homebrew.yml` job — it re-derives everything from the tag and npm. +- **Wrong notes:** edit the GitHub Release body in the UI; nothing downstream depends on it. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..b378ce1 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,82 @@ +# Troubleshooting + +Start with the built-in self-check — it covers the most common failures in one shot: + +```bash +qring doctor # keyring backend, audit log, manifest, policy, qring-mcp on PATH +qring doctor --json # same, machine-readable +``` + +Set `QRING_DEBUG=1` to get full stack traces from any CLI error instead of the one-line message. + +## Keyring backend errors (most common first-run issue) + +q-ring stores secrets in the OS keychain via [`@napi-rs/keyring`](https://github.com/Brooooooklyn/keyring-node): GNOME Keyring / Secret Service on Linux, Keychain on macOS, Credential Manager on Windows. + +**Symptom:** `qring set` / `get` fails with a keyring error, or `qring doctor` reports the keyring probe failed. + +- **Headless Linux / SSH / CI:** there is usually no unlocked Secret Service. Install and start one: + + ```bash + sudo apt install gnome-keyring dbus-x11 # Debian/Ubuntu + dbus-run-session -- sh -c 'echo "" | gnome-keyring-daemon --unlock; exec your-command' + ``` + + For CI pipelines, prefer `qring exec` / `qring ci:validate` on a runner with a keyring, or inject secrets through your CI's native secret store instead of q-ring. + +- **Linux desktop:** make sure the login keyring is unlocked (it usually unlocks with your session). KDE users need `ksecretservice` or GNOME Keyring installed. +- **Docker:** containers have no OS keychain — the Docker image is for the MCP protocol surface and experiments, not durable storage (see the README's Docker section). + +## MCP server not connecting (Cursor / Kiro / Claude Code) + +1. `qring doctor` — confirms `qring-mcp` is on `PATH`. Editors spawn it by bare name; a shell alias is not enough. +2. Install globally so the binary lands on `PATH`: `pnpm add -g @i4ctime/q-ring` (or npm/yarn/brew). +3. GUI editors don't always inherit your shell's `PATH` (especially on macOS when launched from the Dock). If tools don't appear, set the absolute path in the MCP config: + + ```json + { "mcpServers": { "q-ring": { "command": "/absolute/path/to/qring-mcp" } } } + ``` + + Find it with `command -v qring-mcp` (or `where qring-mcp` on Windows). +4. Approve the server when the editor prompts (Claude Code asks per-project for `.mcp.json` servers). +5. Still stuck? Run the handshake by hand — you should get a JSON `serverInfo` response: + + ```bash + echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"0.0.0"}}}' | qring-mcp | head -1 + ``` + +## "Why can't the agent read my secret?" (approval gate) + +Secrets stored with `--requires-approval` (or matching `policy.secrets.requireApprovalForTags`) refuse MCP reads until you grant a scoped, time-boxed approval: + +```bash +qring approve MY_KEY --for 3600 --reason "deploying" +qring approvals # see active grants, TTLs, and tamper status +``` + +The CLI itself is never gated — only MCP/agent reads are. + +## "My policy is being ignored" (MCP policy root pinning) + +Since 0.12.0 the MCP server resolves `.q-ring.json` **policy** from the directory it was launched in, not from the `projectPath` the agent passes (that closed a policy-escape hole). If your policy seems inactive over MCP: + +- make sure the editor launches `qring-mcp` with the project as its working directory, or +- put the policy in the `.q-ring.json` at the server's launch directory. + +Project-scoped *secret* resolution still follows `projectPath` — only policy is pinned. + +## Audit log + +- Default location: `~/.config/q-ring/audit.jsonl`. Override with `QRING_AUDIT_DIR` (created if missing) — useful for tests and sandboxes. +- `qring audit:verify` (or the dashboard's chain badge) checks the tamper-evident hash chain. A broken chain means the file was edited or partially deleted; export what you need (`qring audit:export`), then investigate before trusting the tail. + +## Scripting gotchas + +- Destructive commands (`delete`, `forget --all`, `teleport unpack` over existing keys, `wizard` over existing keys, `env:generate -o` over an existing file) prompt on a TTY and **error** on non-TTY stdin — pass `--yes`/`-y` in scripts. +- Machine output: `--json` works on effectively the whole read surface and emits `{ "ok": true, "data": … }` (see [cli-mcp-parity.md](cli-mcp-parity.md)). +- `qring get --raw` prints the bare value with no trailing newline — right for piping into other tools. +- `qring has KEY --quiet` is the exit-code-only existence check (decay-aware: expired secrets count as absent). + +## AGPL questions + +q-ring is AGPL-3.0-only. Using the CLI/MCP server as a tool in your workflow does **not** make your projects AGPL — the license applies to q-ring itself and derivative works of it. If you modify q-ring and offer it as a network service, you must publish your modifications. Not legal advice; see [LICENSE](../LICENSE). diff --git a/kiro-plugin/steering/qring-exec-with-secrets.md b/kiro-plugin/steering/qring-exec-with-secrets.md index 7254576..76d52e6 100644 --- a/kiro-plugin/steering/qring-exec-with-secrets.md +++ b/kiro-plugin/steering/qring-exec-with-secrets.md @@ -6,7 +6,7 @@ inclusion: manual > Activate by typing `#qring-exec-with-secrets` in chat. -Run shell commands with secrets injected from q-ring — with stdout/stderr redaction and policy checks. +Run shell commands with secrets injected from q-ring with stdout/stderr redaction and policy checks. ## When to use @@ -22,12 +22,11 @@ Run shell commands with secrets injected from q-ring — with stdout/stderr reda ## Workflow -1. Call `get_policy_summary` if unsure whether `exec_with_secrets` is allowed (`policy.mcp` may deny this tool). -2. Prefer `check_policy` with `action: "exec"` when experimenting with deny/allow rules. -3. Call `exec_with_secrets` with the argv array, `projectPath`, and optional key filters so only required secrets are loaded. +1. Call **`get_policy_summary`** if unsure whether `exec_with_secrets` is allowed (`policy.mcp` may deny this tool). +2. Prefer **`check_policy`** with `action: "exec"` when experimenting with deny/allow rules. +3. Call **`exec_with_secrets`** with the argv array, `projectPath`, and optional key filters so only required secrets are loaded. ## Safety - Never log raw command output to public channels without reviewing redaction limits. -- Prefer `tunnel_create` / `tunnel_read` for one-off handoff of a single value to another agent when full `exec` is unnecessary. -- Use exec profiles (`restricted`, `ci`, `unrestricted`) to bound command behavior. +- Prefer **`tunnel_create` / `tunnel_read`** for one-off handoff of a single value to another agent when full `exec` is unnecessary. diff --git a/kiro-plugin/steering/qring-project-onboarding.md b/kiro-plugin/steering/qring-project-onboarding.md index ab29700..01276a3 100644 --- a/kiro-plugin/steering/qring-project-onboarding.md +++ b/kiro-plugin/steering/qring-project-onboarding.md @@ -8,7 +8,7 @@ inclusion: manual Set up q-ring for a new project — create manifests, detect environments, import secrets, configure hooks and policy. -## When to use +## When to Use Activate when the user: - Starts a new project and needs secret management @@ -23,7 +23,7 @@ Activate when the user: Call `detect_environment` to determine the current context. Sources checked in order: 1. Explicit `QRING_ENV` -2. `NODE_ENV` (mapped: `production` → `prod`, `development` → `dev`) +2. `NODE_ENV` (mapped: `production` -> `prod`, `development` -> `dev`) 3. Git branch + `.q-ring.json` `branchMap` (supports globs like `release/*`) 4. `.q-ring.json` `defaultEnv` @@ -61,7 +61,7 @@ Help the user create a `.q-ring.json` file with: If the project has `.env` files, offer to import them with `import_dotenv`. Use `skipExisting: true` to avoid overwriting. -### 5. Generate `.env` from manifest +### 5. Generate .env from manifest Call `env_generate` to produce a `.env` file from the manifest with all declared secrets resolved from q-ring. @@ -72,7 +72,7 @@ Call `register_hook` to set up notifications when secrets change: - HTTP hook: notify a deployment webhook - Signal hook: send SIGHUP to a running process -Use `list_hooks` to inspect the registry and `remove_hook` to delete entries by id. (CLI-only: `qring hook enable|disable|test` for lifecycle control.) +Use **`list_hooks`** to inspect the registry and **`remove_hook`** to delete entries by id. (CLI-only: `qring hook enable|disable|test` for lifecycle control.) ### 7. Configure policy @@ -83,7 +83,7 @@ Guide the user through the `policy` section of `.q-ring.json`: Call `get_policy_summary` to verify the policy is loaded correctly. -## Best practices +## Best Practices - Always create a `.q-ring.json` manifest for team projects — it serves as documentation and validation - Use `branchMap` to automatically detect environments from git branches diff --git a/kiro-plugin/steering/qring-secret-management.md b/kiro-plugin/steering/qring-secret-management.md index b067e92..07e2a3b 100644 --- a/kiro-plugin/steering/qring-secret-management.md +++ b/kiro-plugin/steering/qring-secret-management.md @@ -8,7 +8,7 @@ inclusion: manual Manage secrets stored in q-ring — store, retrieve, list, inspect, import, export, and configure superposition, decay, entanglement, and tags. -## When to use +## When to Use Activate when the user: - Asks to store, retrieve, delete, or list secrets @@ -57,9 +57,9 @@ Link secrets so rotating one updates the other: ### 6. Scopes -Secrets resolve through a cascade: project → team → org → global. Use the `scope` parameter to target a specific level. +Secrets resolve through a cascade: project -> team -> org -> global. Use the `scope` parameter to target a specific level. -## Best practices +## Best Practices - Always add a `description` when storing secrets for discoverability - Use `tags` to group related secrets (e.g., `database`, `api`, `auth`) diff --git a/kiro-plugin/steering/qring-secret-rotation.md b/kiro-plugin/steering/qring-secret-rotation.md index da39645..127f12d 100644 --- a/kiro-plugin/steering/qring-secret-rotation.md +++ b/kiro-plugin/steering/qring-secret-rotation.md @@ -8,7 +8,7 @@ inclusion: manual Validate, rotate, and batch-check secrets against their providers. -## When to use +## When to Use Activate when the user: - Mentions expired, stale, or invalid secrets @@ -61,8 +61,8 @@ Call `list_providers` to see all supported validation providers with their prefi - **`status_dashboard`** — starts the local quantum status dashboard (browser UI on localhost). - **`agent_scan`** — one-shot health scan JSON (same family as CLI `qring agent --once`). -## Best practices +## Best Practices -- Store secrets with `rotationFormat` (e.g., `api-key`, `password`, `uuid`) to enable automatic rotation -- Set `provider` metadata for automatic validation detection +- Store secrets with `--rotation-format` (e.g., `api-key`, `password`, `uuid`) to enable automatic rotation +- Set `--provider` metadata for automatic validation detection - Run `ci_validate_secrets` in CI pipelines to catch expired credentials before deployment diff --git a/kiro-plugin/steering/qring-secret-scanning.md b/kiro-plugin/steering/qring-secret-scanning.md index b6cf89a..9e60945 100644 --- a/kiro-plugin/steering/qring-secret-scanning.md +++ b/kiro-plugin/steering/qring-secret-scanning.md @@ -8,7 +8,7 @@ inclusion: manual Scan and lint codebases for hardcoded secrets using entropy analysis and regex heuristics. -## When to use +## When to Use Activate when the user: - Asks to scan a project for hardcoded secrets or credentials @@ -49,7 +49,7 @@ Call `lint_files` with the list of affected files and `fix: true` to: After fixing, re-run `scan_codebase_for_secrets` to confirm no secrets remain. -## Best practices +## Best Practices - Run scanning before every PR or commit - Ignore placeholder values (`your_api_key`, `replace_me`, `example`) — the scanner handles this automatically diff --git a/package.json b/package.json index 225f44e..62cbd06 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,10 @@ "plugin:sync": "node scripts/plugin-sync.mjs", "plugin:sync:kiro": "node scripts/plugin-sync-kiro.mjs", "plugin:sync:claude": "node scripts/plugin-sync-claude.mjs", + "plugin:gen-skills": "node scripts/plugin-generate-skills.mjs", + "check:parity": "node scripts/check-plugin-parity.mjs && node scripts/plugin-generate-skills.mjs --check", "docs:publish-log": "node scripts/append-changelog-to-publish-log.mjs", + "release:prepare": "node scripts/prepare-release.mjs", "prepublishOnly": "pnpm run build && pnpm run sync-versions" }, "keywords": [ diff --git a/scripts/check-plugin-parity.mjs b/scripts/check-plugin-parity.mjs new file mode 100644 index 0000000..4a4e566 --- /dev/null +++ b/scripts/check-plugin-parity.mjs @@ -0,0 +1,123 @@ +#!/usr/bin/env node +/** + * CI guard: the three editor plugins (Cursor, Claude Code, Kiro) must expose + * the same commands/agents/skills, doc'd MCP tool counts must match the code, + * and plugin manifest versions must match package.json. + * + * node scripts/check-plugin-parity.mjs + * + * Exits 1 with a list of mismatches. Run `pnpm run plugin:gen-skills` and + * `pnpm run sync-versions` to fix generated-content failures. + */ +import { readFileSync, readdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const errors = []; + +const stems = (dir, { strip = "", ext = ".md" } = {}) => + readdirSync(join(root, dir)) + .filter((f) => f.endsWith(ext) && f.startsWith(strip)) + .map((f) => f.slice(strip.length, -ext.length)) + .sort(); + +const dirs = (dir) => + readdirSync(join(root, dir), { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + .sort(); + +function expectEqual(label, a, b) { + if (JSON.stringify(a) !== JSON.stringify(b)) { + errors.push(`${label}: mismatch\n left: ${a.join(", ")}\n right: ${b.join(", ")}`); + } +} + +// ── Commands ────────────────────────────────────────────────────────────── +const cursorCmds = stems("cursor-plugin/commands"); +const claudeCmds = stems("claude-code-plugin/commands", { strip: "qring-" }); +const kiroCmds = stems("kiro-plugin/steering", { strip: "qring-cmd-" }); +expectEqual("commands (cursor vs claude)", cursorCmds, claudeCmds); +expectEqual("commands (cursor vs kiro)", cursorCmds, kiroCmds); + +// ── Agents ──────────────────────────────────────────────────────────────── +expectEqual( + "agents (cursor vs claude)", + stems("cursor-plugin/agents"), + stems("claude-code-plugin/agents"), +); + +// ── Skills ──────────────────────────────────────────────────────────────── +const cursorSkills = dirs("cursor-plugin/skills"); +expectEqual("skills (cursor vs claude)", cursorSkills, dirs("claude-code-plugin/skills")); +const kiroSkillFiles = stems("kiro-plugin/steering", { strip: "qring-" }).filter( + (s) => cursorSkills.includes(s), +); +expectEqual("skills (cursor vs kiro steering)", cursorSkills, kiroSkillFiles); + +// ── MCP tool count: code vs docs ────────────────────────────────────────── +let actualTools = 0; +for (const f of readdirSync(join(root, "src", "mcp", "tools"))) { + if (!f.endsWith(".ts")) continue; + actualTools += ( + readFileSync(join(root, "src", "mcp", "tools", f), "utf8").match( + /server\.tool\(/g, + ) ?? [] + ).length; +} + +const TOOL_COUNT_PATTERNS = [ + /MCP_tools-(\d+)-/g, // README badge + /(\d+)\s+(?:q-ring\s+)?(?:built-in\s+)?MCP tools/g, + /MCP server with (\d+) tools/g, + /all (\d+) tools\b/g, +]; +const DOC_FILES = [ + "README.md", + "cursor-plugin/README.md", + "kiro-plugin/README.md", + "kiro-plugin/POWER.md", + "claude-code-plugin/README.md", + "docs/quickstart-claude-code.md", + "docs/quickstart-cursor.md", + "docs/quickstart-kiro.md", +]; +for (const file of DOC_FILES) { + const text = readFileSync(join(root, file), "utf8"); + for (const pattern of TOOL_COUNT_PATTERNS) { + for (const m of text.matchAll(pattern)) { + const claimed = Number(m[1]); + if (claimed !== actualTools) { + errors.push( + `${file}: claims ${claimed} MCP tools but src/mcp/tools registers ${actualTools} ("${m[0]}")`, + ); + } + } + } +} + +// ── Versions: manifests vs package.json ─────────────────────────────────── +const pkgVersion = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version; +const VERSIONED = [ + ["cursor-plugin/.cursor-plugin/plugin.json", (j) => j.version], + [".cursor-plugin/marketplace.json", (j) => j.plugins?.[0]?.version], + ["claude-code-plugin/.claude-plugin/plugin.json", (j) => j.version], + [".claude-plugin/marketplace.json", (j) => j.plugins?.[0]?.version], + ["server.json", (j) => j.version], +]; +for (const [file, pick] of VERSIONED) { + const v = pick(JSON.parse(readFileSync(join(root, file), "utf8"))); + if (v !== pkgVersion) { + errors.push(`${file}: version ${v} != package.json ${pkgVersion} (run: pnpm run sync-versions)`); + } +} + +if (errors.length > 0) { + console.error(`check-plugin-parity: ${errors.length} problem(s):\n`); + for (const e of errors) console.error(` ✗ ${e}\n`); + process.exit(1); +} +console.log( + `check-plugin-parity: OK — ${cursorCmds.length} commands, ${stems("cursor-plugin/agents").length} agents, ${cursorSkills.length} skills in sync across plugins; ${actualTools} MCP tools match docs; versions ${pkgVersion} in lockstep`, +); diff --git a/scripts/plugin-generate-skills.mjs b/scripts/plugin-generate-skills.mjs new file mode 100644 index 0000000..1ab609f --- /dev/null +++ b/scripts/plugin-generate-skills.mjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node +/** + * Generate the Claude Code and Kiro skill files from the Cursor plugin's + * skills — `cursor-plugin/skills//SKILL.md` is the single source of + * truth. Skills are platform-agnostic content; hand-copying them caused + * silent drift (heading case, arrows) across the three plugins. + * + * node scripts/plugin-generate-skills.mjs # regenerate + * node scripts/plugin-generate-skills.mjs --check # exit 1 if out of date (CI) + * + * Outputs: + * claude-code-plugin/skills//SKILL.md — verbatim copy + * kiro-plugin/steering/qring-.md — Kiro steering transform + * (manual inclusion, #qring- chat tag) + */ +import { readFileSync, writeFileSync, readdirSync, mkdirSync, existsSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const srcDir = join(root, "cursor-plugin", "skills"); +const checkMode = process.argv.includes("--check"); + +// Kiro steering titles are curated per skill (not derivable from the slug). +const KIRO_TITLES = { + "secret-management": "Secret Management", + "secret-scanning": "Secret Scanning", + "secret-rotation": "Secret Rotation & Validation", + "project-onboarding": "Project Onboarding", + "exec-with-secrets": "Exec with Secrets", +}; + +function parseFrontmatter(md) { + const m = md.match(/^---\n([\s\S]*?)\n---\n/); + if (!m) throw new Error("SKILL.md missing frontmatter"); + const desc = m[1].match(/^description:\s*(.*)$/m)?.[1] ?? ""; + return { description: desc, body: md.slice(m[0].length) }; +} + +function kiroSteering(slug, skillMd) { + const { description, body } = parseFrontmatter(skillMd); + const title = KIRO_TITLES[slug] ?? slug; + // Short description: everything before the "Use when …" trigger sentence. + const shortDesc = description.split(/\s+Use when/)[0].trim(); + // Body: everything after the top-level `# Title` heading line. + const afterTitle = body.replace(/^\s*# .*\n/, ""); + return [ + "---", + "inclusion: manual", + "---", + "", + `# q-ring · ${title} (skill)`, + "", + `> Activate by typing \`#qring-${slug}\` in chat.`, + "", + shortDesc, + "", + afterTitle.replace(/^\n+/, ""), + ].join("\n"); +} + +const slugs = readdirSync(srcDir).filter((d) => + existsSync(join(srcDir, d, "SKILL.md")), +); +if (slugs.length === 0) throw new Error(`No skills found in ${srcDir}`); + +let stale = []; + +function emit(path, content) { + if (checkMode) { + const current = existsSync(path) ? readFileSync(path, "utf8") : null; + if (current !== content) stale.push(path); + return; + } + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, content, "utf8"); +} + +for (const slug of slugs) { + const skillMd = readFileSync(join(srcDir, slug, "SKILL.md"), "utf8"); + emit(join(root, "claude-code-plugin", "skills", slug, "SKILL.md"), skillMd); + emit(join(root, "kiro-plugin", "steering", `qring-${slug}.md`), kiroSteering(slug, skillMd)); +} + +if (checkMode) { + if (stale.length > 0) { + console.error("plugin-generate-skills: OUT OF DATE — regenerate with `pnpm run plugin:gen-skills` and commit:"); + for (const p of stale) console.error(` ${p}`); + process.exit(1); + } + console.log(`plugin-generate-skills: ${slugs.length * 2} generated files are up to date`); +} else { + console.log( + `plugin-generate-skills: wrote ${slugs.length} Claude skills + ${slugs.length} Kiro steering files from cursor-plugin/skills`, + ); +} diff --git a/scripts/plugin-sync-claude.mjs b/scripts/plugin-sync-claude.mjs index 60b0500..f75084a 100644 --- a/scripts/plugin-sync-claude.mjs +++ b/scripts/plugin-sync-claude.mjs @@ -16,13 +16,19 @@ * # Force overwrite of existing settings.json, .mcp.json, CLAUDE.md * node scripts/plugin-sync-claude.mjs --force * - * Project-scoped layout: + * NOTE: this script is the manual, project-scoped install path. If you just + * want the plugin, install it through Claude Code's plugin system instead: + * /plugin marketplace add I4cTime/quantum_ring + * /plugin install qring@q-ring + * + * Project-scoped layout (source dirs live at the plugin root, matching the + * Claude Code plugin format; they are copied under .claude/ for project use): * claude-code-plugin/CLAUDE.md -> $DEST/CLAUDE.md * claude-code-plugin/.mcp.json -> $DEST/.mcp.json - * claude-code-plugin/.claude/agents/* -> $DEST/.claude/agents/ - * claude-code-plugin/.claude/commands/* -> $DEST/.claude/commands/ - * claude-code-plugin/.claude/skills/* -> $DEST/.claude/skills/ - * claude-code-plugin/.claude/hooks/* -> $DEST/.claude/hooks/ + * claude-code-plugin/agents/* -> $DEST/.claude/agents/ + * claude-code-plugin/commands/* -> $DEST/.claude/commands/ + * claude-code-plugin/skills/* -> $DEST/.claude/skills/ + * claude-code-plugin/hooks/* -> $DEST/.claude/hooks/ (except hooks.json, plugin-only) * claude-code-plugin/.claude/settings.json -> $DEST/.claude/settings.json * * Files inside agents/, commands/, skills/, and hooks/ are namespaced (qring-*, @@ -82,12 +88,15 @@ const writeOrTemplate = (from, to) => { const subdirs = ["agents", "commands", "skills", "hooks"]; for (const sub of subdirs) { - const srcDir = join(claudeSrc, sub); + const srcDir = join(src, sub); if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) continue; const destDir = join(claudeDest, sub); mkdirSync(destDir, { recursive: true }); for (const entry of readdirSync(srcDir)) { if (entry.startsWith(".")) continue; + // hooks.json is the plugin-format manifest (uses ${CLAUDE_PLUGIN_ROOT}); + // project installs wire hooks via .claude/settings.json instead. + if (sub === "hooks" && entry === "hooks.json") continue; const from = join(srcDir, entry); const to = join(destDir, entry); cpSync(from, to, { recursive: true }); diff --git a/scripts/prepare-release.mjs b/scripts/prepare-release.mjs new file mode 100644 index 0000000..1d99913 --- /dev/null +++ b/scripts/prepare-release.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node +/** + * Prepare a release in one step: + * + * pnpm run release:prepare patch # 0.12.0 -> 0.12.1 + * pnpm run release:prepare minor # 0.12.0 -> 0.13.0 + * pnpm run release:prepare major # 0.12.0 -> 1.0.0 + * pnpm run release:prepare 0.13.0 # explicit version + * + * What it does: + * 1. bumps package.json version + * 2. runs sync-versions (plugin manifests, server.json, SECURITY.md) + * 3. rolls CHANGELOG.md: [Unreleased] -> [x.y.z] — YYYY-MM-DD, fresh [Unreleased] + * 4. runs the parity check + * 5. prints the commit/tag/push steps — pushing the tag triggers + * .github/workflows/release.yml, which creates the GitHub Release and + * chains npm + MCP Registry publish and the Homebrew tap update. + */ +import { readFileSync, writeFileSync } from "node:fs"; +import { execSync } from "node:child_process"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const arg = process.argv[2]; + +if (!arg) { + console.error("usage: prepare-release.mjs "); + process.exit(1); +} + +const pkgPath = join(root, "package.json"); +const pkg = JSON.parse(readFileSync(pkgPath, "utf8")); +const [maj, min, pat] = pkg.version.split(".").map(Number); + +let next; +if (arg === "patch") next = `${maj}.${min}.${pat + 1}`; +else if (arg === "minor") next = `${maj}.${min + 1}.0`; +else if (arg === "major") next = `${maj + 1}.0.0`; +else if (/^\d+\.\d+\.\d+$/.test(arg)) next = arg; +else { + console.error(`prepare-release: "${arg}" is not patch|minor|major or x.y.z`); + process.exit(1); +} + +// 1. package.json +pkg.version = next; +writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8"); +console.log(`✓ package.json ${maj}.${min}.${pat} -> ${next}`); + +// 2. manifests +execSync("node scripts/sync-versions.mjs", { cwd: root, stdio: "inherit" }); + +// 3. CHANGELOG roll +const changelogPath = join(root, "CHANGELOG.md"); +let changelog = readFileSync(changelogPath, "utf8"); +if (!changelog.includes("## [Unreleased]")) { + console.error("prepare-release: CHANGELOG.md has no [Unreleased] section"); + process.exit(1); +} +const unreleasedBody = changelog + .split("## [Unreleased]")[1] + .split(/\n## \[/)[0] + .trim(); +if (!unreleasedBody) { + console.error("prepare-release: [Unreleased] section is empty — nothing to release"); + process.exit(1); +} +const today = new Date().toISOString().slice(0, 10); +changelog = changelog.replace( + "## [Unreleased]", + `## [Unreleased]\n\n## [${next}] — ${today}`, +); +writeFileSync(changelogPath, changelog, "utf8"); +console.log(`✓ CHANGELOG.md: [Unreleased] rolled into [${next}] — ${today}`); + +// 4. guards +execSync("node scripts/check-plugin-parity.mjs", { cwd: root, stdio: "inherit" }); + +// 5. next steps +console.log(` +Release ${next} is staged. Review the diff, then: + + git add -A + git commit -m "release: v${next}" + git tag v${next} + git push && git push --tags + +Pushing the tag runs the Release workflow: GitHub Release (notes from the +CHANGELOG section) -> npm + MCP Registry publish -> Homebrew tap update. +`); diff --git a/scripts/sync-versions.mjs b/scripts/sync-versions.mjs index ad39af5..95da613 100644 --- a/scripts/sync-versions.mjs +++ b/scripts/sync-versions.mjs @@ -24,6 +24,18 @@ if (marketplace.plugins?.[0]) { } writeFileSync(marketplacePath, JSON.stringify(marketplace, null, 2) + "\n", "utf8"); +// Claude Code plugin manifest + marketplace (mirrors the Cursor pair above). +const claudePluginPath = join(root, "claude-code-plugin", ".claude-plugin", "plugin.json"); +const claudePlugin = JSON.parse(readFileSync(claudePluginPath, "utf8")); +claudePlugin.version = v; +writeFileSync(claudePluginPath, JSON.stringify(claudePlugin, null, 2) + "\n", "utf8"); + +const claudeMarketplacePath = join(root, ".claude-plugin", "marketplace.json"); +const claudeMarketplace = JSON.parse(readFileSync(claudeMarketplacePath, "utf8")); +if (claudeMarketplace.metadata) claudeMarketplace.metadata.version = v; +for (const p of claudeMarketplace.plugins ?? []) p.version = v; +writeFileSync(claudeMarketplacePath, JSON.stringify(claudeMarketplace, null, 2) + "\n", "utf8"); + // server.json drives the MCP Registry publish — keep its top-level version and // every npm package entry in lockstep, or `mcp-publisher` rejects a stale or // duplicate version at release time. @@ -47,5 +59,5 @@ sec = sec.replace( writeFileSync(securityPath, sec, "utf8"); console.log( - `sync-versions: set ${v} in plugin.json, marketplace.json, server.json, SECURITY.md`, + `sync-versions: set ${v} in cursor + claude plugin.json/marketplace.json, server.json, SECURITY.md`, ); diff --git a/src/__tests__/core/approval.test.ts b/src/__tests__/core/approval.test.ts index 6b6729b..e76a0cd 100644 --- a/src/__tests__/core/approval.test.ts +++ b/src/__tests__/core/approval.test.ts @@ -10,18 +10,25 @@ import { describe("approval HMAC", () => { let prevHome: string | undefined; + let prevUserProfile: string | undefined; let dir: string; beforeEach(() => { prevHome = process.env.HOME; + prevUserProfile = process.env.USERPROFILE; dir = join(tmpdir(), `qring-approval-${Date.now()}-${Math.random().toString(16).slice(2)}`); mkdirSync(dir, { recursive: true }); + // os.homedir() reads HOME on POSIX but USERPROFILE on Windows — set both + // so the approval store is actually redirected on every platform. process.env.HOME = dir; + process.env.USERPROFILE = dir; }); afterEach(() => { if (prevHome === undefined) delete process.env.HOME; else process.env.HOME = prevHome; + if (prevUserProfile === undefined) delete process.env.USERPROFILE; + else process.env.USERPROFILE = prevUserProfile; rmSync(dir, { recursive: true, force: true }); }); diff --git a/src/__tests__/core/dashboard-server.test.ts b/src/__tests__/core/dashboard-server.test.ts new file mode 100644 index 0000000..6ab86f7 --- /dev/null +++ b/src/__tests__/core/dashboard-server.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { startDashboardServer } from "../../core/dashboard.js"; + +describe("dashboard server auth + security headers", () => { + let dash: ReturnType; + let base: string; + + beforeAll(async () => { + // Port 0 = ephemeral port from the OS; read the real one off the server. + dash = startDashboardServer({ port: 0 }); + await new Promise((resolve) => dash.server.once("listening", resolve)); + const addr = dash.server.address(); + if (addr === null || typeof addr === "string") throw new Error("no address"); + base = `http://127.0.0.1:${addr.port}`; + }); + + afterAll(() => { + dash.close(); + }); + + it("rejects requests without a token", async () => { + const res = await fetch(`${base}/`); + expect(res.status).toBe(403); + }); + + it("rejects requests with a wrong token", async () => { + const res = await fetch(`${base}/?token=not-the-token`); + expect(res.status).toBe(403); + }); + + it("serves the dashboard with the launch token", async () => { + const res = await fetch(`${base}/?token=${dash.token}`); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toContain("text/html"); + }); + + it("includes audit-chain integrity in the snapshot", async () => { + const res = await fetch(`${base}/api/status?token=${dash.token}`); + expect(res.status).toBe(200); + const snap = (await res.json()) as { + auditChain: { intact: boolean; totalEvents: number; validEvents: number }; + }; + expect(snap.auditChain).toBeDefined(); + expect(typeof snap.auditChain.intact).toBe("boolean"); + expect(snap.auditChain.totalEvents).toBeGreaterThanOrEqual(0); + }); + + it("serves the entanglement graph and chain badge styles", async () => { + const res = await fetch(`${base}/?token=${dash.token}`); + const html = await res.text(); + expect(html).toContain(".entangle-graph"); + expect(html).toContain(".chain-badge"); + expect(html).toContain("chain intact"); // client bundle renders the badge + }); + + it("sends security headers on every response", async () => { + for (const url of [`${base}/`, `${base}/?token=${dash.token}`]) { + const res = await fetch(url); + expect(res.headers.get("content-security-policy")).toContain( + "default-src 'none'", + ); + expect(res.headers.get("x-frame-options")).toBe("DENY"); + expect(res.headers.get("x-content-type-options")).toBe("nosniff"); + expect(res.headers.get("referrer-policy")).toBe("no-referrer"); + expect(res.headers.get("cache-control")).toBe("no-store"); + } + }); +}); diff --git a/src/cli/commands.ts b/src/cli/commands.ts index 6bb0d1f..6a93d53 100644 --- a/src/cli/commands.ts +++ b/src/cli/commands.ts @@ -10,6 +10,8 @@ import { registerAuditCommands } from "./commands/audit.js"; import { registerHookCommands } from "./commands/hooks.js"; import { registerAgentCommands } from "./commands/agent.js"; import { registerSecurityCommands } from "./commands/security.js"; +import { registerDoctorCommand } from "./commands/doctor.js"; +import { registerCompletionCommand } from "./commands/completion.js"; /** * Display groups for the CLI help screen. @@ -28,6 +30,7 @@ const COMMAND_GROUPS: Array<{ commands: [ "set", "get", + "has", "delete", "list", "inspect", @@ -59,7 +62,7 @@ const COMMAND_GROUPS: Array<{ { name: "Dev Tooling", symbol: SYMBOLS.zap, - commands: ["exec", "scan", "lint", "status"], + commands: ["exec", "scan", "lint", "status", "doctor", "completion"], }, { name: "Audit & Health", @@ -231,6 +234,8 @@ export function createProgram(): Command { registerHookCommands(program); registerAgentCommands(program); registerSecurityCommands(program); + registerDoctorCommand(program); + registerCompletionCommand(program); return program; } diff --git a/src/cli/commands/agent.ts b/src/cli/commands/agent.ts index 698789d..1d4f64b 100644 --- a/src/cli/commands/agent.ts +++ b/src/cli/commands/agent.ts @@ -10,7 +10,8 @@ import { clearMemory, } from "../../core/memory.js"; import { c, SYMBOLS } from "../../utils/colors.js"; -import { safeStr } from "../helpers.js"; +import { safeStr, emitJson } from "../helpers.js"; +import { confirm } from "../../utils/prompt.js"; import { buildOpts } from "../options.js"; export function registerAgentCommands(program: Command): void { @@ -29,9 +30,19 @@ export function registerAgentCommands(program: Command): void { program .command("recall [key]") .description("Retrieve a value from agent memory, or list all keys") - .action((key?: string) => { + .option("--json", "Output as JSON (list mode only)") + .action((key: string | undefined, cmd) => { if (!key) { const entries = listMemory(); + if ( + emitJson( + program, + cmd, + entries.map((e) => ({ key: e.key, updatedAt: e.updatedAt })), + ) + ) { + return; + } if (entries.length === 0) { console.log(c.dim("Agent memory is empty.")); return; @@ -60,8 +71,20 @@ export function registerAgentCommands(program: Command): void { .command("forget ") .description("Delete a key from agent memory") .option("--all", "Clear all agent memory") - .action((key: string, cmd) => { + .option("-y, --yes", "Skip the confirmation prompt") + .action(async (key: string, cmd) => { if (cmd.all) { + const count = listMemory().length; + if ( + !(await confirm( + `Clear ALL agent memory (${count} entr${count === 1 ? "y" : "ies"})? This cannot be undone.`, + { assumeYes: cmd.yes }, + )) + ) { + console.error(c.dim("Aborted.")); + process.exitCode = 1; + return; + } clearMemory(); console.log(`${SYMBOLS.check} ${c.yellow("cleared")} all agent memory`); return; @@ -84,13 +107,12 @@ export function registerAgentCommands(program: Command): void { .option("-g, --global", "Global scope only") .option("-p, --project", "Project scope only") .option("--project-path ", "Explicit project path") + .option("--json", "Output as JSON") .action((cmd) => { const opts = buildOpts(cmd); const entries = listSecrets({ ...opts, silent: true }); const audit = queryAudit({ limit: 1000 }); - console.log(`\n${SYMBOLS.zap} ${c.bold("Secret Usage Analysis")}\n`); - const accessMap = new Map(); for (const e of audit) { if (e.action === "read" && e.key) { @@ -99,6 +121,44 @@ export function registerAgentCommands(program: Command): void { } const sorted = [...accessMap.entries()].sort((a, b) => b[1] - a[1]); + const neverAccessed = entries.filter((e) => { + const count = e.envelope?.meta.accessCount ?? 0; + return count === 0; + }); + const jsonExpired = entries.filter((e) => e.decay?.isExpired); + const jsonStale = entries.filter( + (e) => e.decay?.isStale && !e.decay?.isExpired, + ); + const jsonNoRotation = entries.filter( + (e) => !e.envelope?.meta.rotationFormat && !e.decay?.isExpired, + ); + + if ( + emitJson(program, cmd, { + mostAccessed: sorted + .slice(0, 5) + .map(([key, reads]) => ({ key, reads })), + neverAccessed: neverAccessed.map((e) => e.key), + expired: jsonExpired.map((e) => e.key), + stale: jsonStale.map((e) => ({ + key: e.key, + timeRemaining: e.decay?.timeRemaining ?? null, + })), + summary: { + total: entries.length, + active: entries.length - jsonExpired.length, + expired: jsonExpired.length, + stale: jsonStale.length, + neverAccessed: neverAccessed.length, + withRotationConfig: entries.length - jsonNoRotation.length, + }, + }) + ) { + return; + } + + console.log(`\n${SYMBOLS.zap} ${c.bold("Secret Usage Analysis")}\n`); + if (sorted.length > 0) { console.log(` ${c.bold("Most accessed:")}`); for (const [key, count] of sorted.slice(0, 5)) { @@ -107,10 +167,6 @@ export function registerAgentCommands(program: Command): void { console.log(); } - const neverAccessed = entries.filter((e) => { - const count = e.envelope?.meta.accessCount ?? 0; - return count === 0; - }); if (neverAccessed.length > 0) { console.log( ` ${c.bold("Never accessed:")} ${c.yellow(neverAccessed.length.toString())} secrets`, diff --git a/src/cli/commands/audit.ts b/src/cli/commands/audit.ts index 1d3ce4e..ef523c1 100644 --- a/src/cli/commands/audit.ts +++ b/src/cli/commands/audit.ts @@ -8,6 +8,7 @@ import { } from "../../core/observer.js"; import { writeFileSync } from "node:fs"; import { c, SYMBOLS } from "../../utils/colors.js"; +import { emitJson } from "../helpers.js"; import { buildOpts } from "../options.js"; export function registerAuditCommands(program: Command): void { @@ -21,9 +22,11 @@ export function registerAuditCommands(program: Command): void { ) .option("-n, --limit ", "Number of events to show", parseInt, 20) .option("--anomalies", "Detect access anomalies") + .option("--json", "Output as JSON") .action((cmd) => { if (cmd.anomalies) { const anomalies = detectAnomalies(cmd.key); + if (emitJson(program, cmd, { anomalies })) return; if (anomalies.length === 0) { console.log(`${SYMBOLS.shield} ${c.green("No anomalies detected")}`); return; @@ -45,6 +48,8 @@ export function registerAuditCommands(program: Command): void { limit: cmd.limit, }); + if (emitJson(program, cmd, { events })) return; + if (events.length === 0) { console.log(c.dim("No audit events found")); return; @@ -81,8 +86,13 @@ export function registerAuditCommands(program: Command): void { program .command("audit:verify") .description("Verify the integrity of the audit hash chain") - .action(() => { + .option("--json", "Output as JSON") + .action((cmd) => { const result = verifyAuditChain(); + if (emitJson(program, cmd, result)) { + if (!result.intact && result.totalEvents > 0) process.exitCode = 1; + return; + } if (result.totalEvents === 0) { console.log(c.dim(" No audit events to verify")); return; @@ -140,45 +150,65 @@ export function registerAuditCommands(program: Command): void { .option("-g, --global", "Check global scope only") .option("-p, --project", "Check project scope only") .option("--project-path ", "Explicit project path") + .option("--json", "Output as JSON") .action((cmd) => { const opts = buildOpts(cmd); const entries = listSecrets(opts); - if (entries.length === 0) { - console.log(c.dim("No secrets to check")); - return; - } - - console.log(c.bold(`\n ${SYMBOLS.shield} Secret health report\n`)); - + const expiredKeys: string[] = []; + const staleKeys: Array<{ key: string; lifetimePercent: number; timeRemaining: string }> = []; let healthy = 0; - let stale = 0; - let expired = 0; let noDecay = 0; for (const entry of entries) { if (!entry.decay || !entry.decay.timeRemaining) { noDecay++; - continue; - } - - if (entry.decay.isExpired) { - expired++; - console.log( - ` ${c.red(SYMBOLS.cross)} ${c.bold(entry.key)} ${c.bgRed(c.white(" EXPIRED "))}`, - ); + } else if (entry.decay.isExpired) { + expiredKeys.push(entry.key); } else if (entry.decay.isStale) { - stale++; - console.log( - ` ${c.yellow(SYMBOLS.warning)} ${c.bold(entry.key)} ${c.yellow(`stale (${entry.decay.lifetimePercent}%, ${entry.decay.timeRemaining} left)`)}`, - ); + staleKeys.push({ + key: entry.key, + lifetimePercent: entry.decay.lifetimePercent, + timeRemaining: entry.decay.timeRemaining, + }); } else { healthy++; } } + if ( + emitJson(program, cmd, { + total: entries.length, + healthy, + noDecay, + expired: expiredKeys, + stale: staleKeys, + anomalies: entries.length === 0 ? [] : detectAnomalies(), + }) + ) { + return; + } + + if (entries.length === 0) { + console.log(c.dim("No secrets to check")); + return; + } + + console.log(c.bold(`\n ${SYMBOLS.shield} Secret health report\n`)); + + for (const key of expiredKeys) { + console.log( + ` ${c.red(SYMBOLS.cross)} ${c.bold(key)} ${c.bgRed(c.white(" EXPIRED "))}`, + ); + } + for (const s of staleKeys) { + console.log( + ` ${c.yellow(SYMBOLS.warning)} ${c.bold(s.key)} ${c.yellow(`stale (${s.lifetimePercent}%, ${s.timeRemaining} left)`)}`, + ); + } + console.log( - `\n ${c.green(`${SYMBOLS.check} ${healthy} healthy`)} ${c.yellow(`${SYMBOLS.warning} ${stale} stale`)} ${c.red(`${SYMBOLS.cross} ${expired} expired`)} ${c.dim(`${noDecay} no decay`)}`, + `\n ${c.green(`${SYMBOLS.check} ${healthy} healthy`)} ${c.yellow(`${SYMBOLS.warning} ${staleKeys.length} stale`)} ${c.red(`${SYMBOLS.cross} ${expiredKeys.length} expired`)} ${c.dim(`${noDecay} no decay`)}`, ); const anomalies = detectAnomalies(); diff --git a/src/cli/commands/completion.ts b/src/cli/commands/completion.ts new file mode 100644 index 0000000..8b5a915 --- /dev/null +++ b/src/cli/commands/completion.ts @@ -0,0 +1,151 @@ +import type { Command } from "commander"; +import { c } from "../../utils/colors.js"; + +/** + * Shell completion scripts generated from the live commander program, so + * they can never drift from the actual command surface. + * + * qring completion bash >> ~/.bashrc (or bash-completion dir) + * qring completion zsh > ~/.zsh/completions/_qring + * qring completion fish > ~/.config/fish/completions/qring.fish + */ + +interface CmdInfo { + name: string; + aliases: string[]; + description: string; + options: Array<{ flag: string; description: string }>; + subcommands: CmdInfo[]; +} + +function collect(cmd: Command): CmdInfo { + return { + name: cmd.name(), + aliases: cmd.aliases(), + description: cmd.description(), + options: cmd.options + .map((o) => ({ flag: o.long ?? o.short ?? "", description: o.description ?? "" })) + .filter((o) => o.flag.startsWith("--")), + subcommands: cmd.commands + .filter((s) => s.name() !== "help") + .map((s) => collect(s as Command)), + }; +} + +const sanitize = (s: string) => + s.replace(/['"`$\\]/g, "").replace(/[:[\]]/g, " ").replace(/\s+/g, " ").trim(); + +function bashScript(cmds: CmdInfo[]): string { + const names = cmds.flatMap((x) => [x.name, ...x.aliases]).join(" "); + const cases = cmds + .map((x) => { + const label = [x.name, ...x.aliases].join("|"); + const opts = [...x.options.map((o) => o.flag), "--json"].join(" "); + const subs = x.subcommands.map((s) => s.name).join(" "); + const subCase = subs + ? `\n if [ "$COMP_CWORD" -eq 2 ]; then COMPREPLY=( $(compgen -W "${subs}" -- "$cur") ); return; fi` + : ""; + return ` ${label})${subCase}\n opts="${opts}" ;;`; + }) + .join("\n"); + + return `# bash completion for qring — generated by \`qring completion bash\` +_qring_completions() { + local cur cmd opts + cur="\${COMP_WORDS[COMP_CWORD]}" + cmd="\${COMP_WORDS[1]}" + if [ "$COMP_CWORD" -eq 1 ]; then + COMPREPLY=( $(compgen -W "${names}" -- "$cur") ) + return + fi + opts="--json --help" + case "$cmd" in +${cases} + esac + case "$cur" in + -*) COMPREPLY=( $(compgen -W "$opts --help" -- "$cur") ) ;; + esac +} +complete -o default -F _qring_completions qring +`; +} + +function zshScript(cmds: CmdInfo[]): string { + const items = cmds + .map((x) => ` '${x.name}:${sanitize(x.description)}'`) + .join("\n"); + const cases = cmds + .map((x) => { + const label = [x.name, ...x.aliases].join("|"); + const words = [ + ...x.subcommands.map((s) => s.name), + ...x.options.map((o) => o.flag), + "--json", + ].join(" "); + return ` ${label}) compadd -- ${words} ;;`; + }) + .join("\n"); + + return `#compdef qring +# zsh completion for qring — generated by \`qring completion zsh\` +_qring() { + local -a commands + commands=( +${items} + ) + if (( CURRENT == 2 )); then + _describe 'command' commands + return + fi + case $words[2] in +${cases} + esac +} +_qring "$@" +`; +} + +function fishScript(cmds: CmdInfo[]): string { + const lines: string[] = [ + "# fish completion for qring — generated by `qring completion fish`", + "complete -c qring -f", + ]; + const allNames = cmds.flatMap((x) => [x.name, ...x.aliases]); + for (const x of cmds) { + for (const name of [x.name, ...x.aliases]) { + lines.push( + `complete -c qring -n "not __fish_seen_subcommand_from ${allNames.join(" ")}" -a ${name} -d "${sanitize(x.description)}"`, + ); + } + for (const s of x.subcommands) { + lines.push( + `complete -c qring -n "__fish_seen_subcommand_from ${x.name}" -a ${s.name} -d "${sanitize(s.description)}"`, + ); + } + for (const o of [...x.options, { flag: "--json", description: "Output as JSON" }]) { + lines.push( + `complete -c qring -n "__fish_seen_subcommand_from ${x.name}" -l ${o.flag.slice(2)} -d "${sanitize(o.description)}"`, + ); + } + } + return lines.join("\n") + "\n"; +} + +export function registerCompletionCommand(program: Command): void { + program + .command("completion ") + .description("Print a shell completion script (bash, zsh, fish)") + .action((shell: string) => { + const cmds = program.commands + .filter((s) => s.name() !== "help" && s.name() !== "completion") + .map((s) => collect(s as Command)); + + if (shell === "bash") process.stdout.write(bashScript(cmds)); + else if (shell === "zsh") process.stdout.write(zshScript(cmds)); + else if (shell === "fish") process.stdout.write(fishScript(cmds)); + else { + console.error(c.red(`Unknown shell "${shell}" — expected bash, zsh, or fish`)); + process.exitCode = 1; + } + }); +} diff --git a/src/cli/commands/doctor.ts b/src/cli/commands/doctor.ts new file mode 100644 index 0000000..9bfb7c2 --- /dev/null +++ b/src/cli/commands/doctor.ts @@ -0,0 +1,215 @@ +import type { Command } from "commander"; +import { existsSync, readFileSync, writeFileSync, rmSync } from "node:fs"; +import { join, delimiter } from "node:path"; +import { homedir } from "node:os"; +import { verifyAuditChain } from "../../core/observer.js"; +import { getPolicySummary } from "../../core/policy.js"; +import { c, SYMBOLS } from "../../utils/colors.js"; +import { emitJson } from "../helpers.js"; +import { PACKAGE_VERSION } from "../../version.js"; + +type CheckStatus = "ok" | "warn" | "fail"; + +interface CheckResult { + name: string; + status: CheckStatus; + detail: string; +} + +function auditDir(): string { + return process.env.QRING_AUDIT_DIR ?? join(homedir(), ".config", "q-ring"); +} + +function checkNode(): CheckResult { + const major = Number(process.versions.node.split(".")[0]); + return { + name: "node runtime", + status: major >= 18 ? "ok" : "fail", + detail: `v${process.versions.node}${major >= 18 ? "" : " — q-ring requires Node >= 18"}`, + }; +} + +async function checkKeyringBackend(): Promise { + // Probe the OS keychain end-to-end with a dedicated service name so real + // secrets and the audit log are never touched. This is the check that + // fails on headless Linux without a Secret Service (gnome-keyring, etc.). + try { + const { Entry } = await import("@napi-rs/keyring"); + const probe = new Entry("q-ring-doctor-probe", "PROBE"); + probe.setPassword("ok"); + const read = probe.getPassword(); + probe.deletePassword(); + if (read !== "ok") { + return { + name: "keyring backend", + status: "fail", + detail: "probe round-trip returned unexpected value", + }; + } + return { + name: "keyring backend", + status: "ok", + detail: "write/read/delete round-trip succeeded", + }; + } catch (err) { + return { + name: "keyring backend", + status: "fail", + detail: `${err instanceof Error ? err.message : String(err)} — on headless Linux, install and unlock a Secret Service (e.g. gnome-keyring)`, + }; + } +} + +function checkAuditLog(): CheckResult { + const dir = auditDir(); + try { + const probeFile = join(dir, ".doctor-probe"); + writeFileSync(probeFile, "ok"); + rmSync(probeFile); + } catch (err) { + return { + name: "audit log", + status: "fail", + detail: `${dir} not writable: ${err instanceof Error ? err.message : String(err)}`, + }; + } + + const chain = verifyAuditChain(); + if (chain.totalEvents === 0) { + return { name: "audit log", status: "ok", detail: `${dir} writable, no events yet` }; + } + if (!chain.intact) { + return { + name: "audit log", + status: "fail", + detail: `hash chain BROKEN at event #${chain.brokenAt} (${chain.validEvents}/${chain.totalEvents} valid) — see qring audit:verify`, + }; + } + return { + name: "audit log", + status: "ok", + detail: `${dir} writable, chain intact (${chain.totalEvents} events)`, + }; +} + +function checkManifest(projectPath: string): CheckResult { + const manifestPath = join(projectPath, ".q-ring.json"); + if (!existsSync(manifestPath)) { + return { + name: "project manifest", + status: "warn", + detail: `no .q-ring.json in ${projectPath} (optional — run qring wizard or /qring-setup-project)`, + }; + } + try { + const config = JSON.parse(readFileSync(manifestPath, "utf8")); + const declared = Object.keys(config.secrets ?? {}).length; + const hasPolicy = !!config.policy; + return { + name: "project manifest", + status: "ok", + detail: `${declared} secret(s) declared${hasPolicy ? ", policy present" : ""}`, + }; + } catch (err) { + return { + name: "project manifest", + status: "fail", + detail: `.q-ring.json is not valid JSON: ${err instanceof Error ? err.message : String(err)}`, + }; + } +} + +function checkPolicy(): CheckResult { + const summary = getPolicySummary(); + const active = [ + summary.hasMcpPolicy && "mcp", + summary.hasExecPolicy && "exec", + summary.hasSecretPolicy && "secrets", + ].filter(Boolean); + return { + name: "governance policy", + status: "ok", + detail: active.length + ? `active: ${active.join(", ")}` + : "none configured (add a \"policy\" section to .q-ring.json to enable)", + }; +} + +function checkMcpBinary(): CheckResult { + const exts = process.platform === "win32" ? [".cmd", ".exe", ".bat", ""] : [""]; + for (const dir of (process.env.PATH ?? "").split(delimiter)) { + if (!dir) continue; + for (const ext of exts) { + if (existsSync(join(dir, `qring-mcp${ext}`))) { + return { + name: "qring-mcp binary", + status: "ok", + detail: `found at ${join(dir, `qring-mcp${ext}`)}`, + }; + } + } + } + return { + name: "qring-mcp binary", + status: "warn", + detail: + "not on PATH — editor MCP configs that spawn `qring-mcp` will fail; install globally (pnpm add -g @i4ctime/q-ring)", + }; +} + +export function registerDoctorCommand(program: Command): void { + program + .command("doctor") + .description("Diagnose the q-ring installation (keyring, audit, manifest, MCP)") + .option("--project-path ", "Project path (defaults to cwd)") + .option("--json", "Output as JSON") + .action(async (cmd) => { + const projectPath = cmd.projectPath ?? process.cwd(); + + const checks: CheckResult[] = [ + checkNode(), + await checkKeyringBackend(), + checkAuditLog(), + checkManifest(projectPath), + checkPolicy(), + checkMcpBinary(), + ]; + + const failed = checks.filter((r) => r.status === "fail").length; + const warned = checks.filter((r) => r.status === "warn").length; + + if ( + emitJson(program, cmd, { + version: PACKAGE_VERSION, + projectPath, + checks, + healthy: failed === 0, + }) + ) { + if (failed > 0) process.exitCode = 1; + return; + } + + console.log(c.bold(`\n ${SYMBOLS.shield} qring doctor — v${PACKAGE_VERSION}\n`)); + + for (const r of checks) { + const icon = + r.status === "ok" + ? c.green(SYMBOLS.check) + : r.status === "warn" + ? c.yellow(SYMBOLS.warning) + : c.red(SYMBOLS.cross); + console.log(` ${icon} ${c.bold(r.name.padEnd(18))} ${c.dim(r.detail)}`); + } + + console.log(); + if (failed > 0) { + console.log(` ${c.red(`${failed} check(s) failed`)}${warned ? c.dim(`, ${warned} warning(s)`) : ""}\n`); + process.exitCode = 1; + } else if (warned > 0) { + console.log(` ${c.yellow(`${warned} warning(s)`)} ${c.dim("— everything critical is healthy")}\n`); + } else { + console.log(` ${c.green("All checks passed.")}\n`); + } + }); +} diff --git a/src/cli/commands/hooks.ts b/src/cli/commands/hooks.ts index 1e8a59f..bf21e8c 100644 --- a/src/cli/commands/hooks.ts +++ b/src/cli/commands/hooks.ts @@ -15,6 +15,7 @@ import { runPreCommitScan, } from "../../hooks/precommit.js"; import { c, SYMBOLS } from "../../utils/colors.js"; +import { emitJson } from "../helpers.js"; export function registerHookCommands(program: Command): void { // ─── Pre-Commit Hook ─── @@ -140,8 +141,10 @@ export function registerHookCommands(program: Command): void { .command("list") .alias("ls") .description("List all registered hooks") - .action(() => { + .option("--json", "Output as JSON") + .action((cmd) => { const hooks = listAllHooks(); + if (emitJson(program, cmd, { hooks })) return; if (hooks.length === 0) { console.log(c.dim("No hooks registered")); return; diff --git a/src/cli/commands/project.ts b/src/cli/commands/project.ts index 255c9d3..d32b032 100644 --- a/src/cli/commands/project.ts +++ b/src/cli/commands/project.ts @@ -3,6 +3,7 @@ import { getSecret, setSecret, getEnvelope, + hasSecret, } from "../../core/keyring.js"; import { collapseEnvironment, readProjectConfig } from "../../core/collapse.js"; import { checkDecay } from "../../core/envelope.js"; @@ -11,7 +12,8 @@ import { getProjectContext } from "../../core/context.js"; import { registerHook, type HookType, type HookAction } from "../../core/hooks.js"; import { writeFileSync, readFileSync, existsSync } from "node:fs"; import { c, scopeColor, envBadge, SYMBOLS } from "../../utils/colors.js"; -import { wantsJsonOutput } from "../helpers.js"; +import { wantsJsonOutput, emitJson } from "../helpers.js"; +import { confirm } from "../../utils/prompt.js"; import { buildOpts } from "../options.js"; export function registerProjectCommands(program: Command): void { @@ -102,6 +104,7 @@ export function registerProjectCommands(program: Command): void { .command("check") .description("Validate project secrets against .q-ring.json manifest") .option("--project-path ", "Project path (defaults to cwd)") + .option("--json", "Output as JSON") .action((cmd) => { const projectPath = cmd.projectPath ?? process.cwd(); const config = readProjectConfig(projectPath); @@ -118,53 +121,74 @@ export function registerProjectCommands(program: Command): void { process.exit(1); } - console.log( - c.bold(`\n ${SYMBOLS.shield} Project secret manifest check\n`), - ); - - let present = 0; - let missing = 0; - let expiredCount = 0; - let staleCount = 0; + type KeyStatus = "ok" | "missing" | "optional-not-set" | "expired" | "stale"; + const statuses: Array<{ + key: string; + status: KeyStatus; + description?: string; + lifetimePercent?: number; + }> = []; for (const [key, manifest] of Object.entries(config.secrets)) { const result = getEnvelope(key, { projectPath, source: "cli" }); if (!result) { - if (manifest.required !== false) { - missing++; - console.log( - ` ${c.red(SYMBOLS.cross)} ${c.bold(key)} ${c.red("MISSING")} ${manifest.description ? c.dim(`— ${manifest.description}`) : ""}`, - ); - } else { - console.log( - ` ${c.dim(SYMBOLS.cross)} ${c.bold(key)} ${c.dim("optional, not set")} ${manifest.description ? c.dim(`— ${manifest.description}`) : ""}`, - ); - } + statuses.push({ + key, + status: manifest.required !== false ? "missing" : "optional-not-set", + description: manifest.description, + }); continue; } const decay = checkDecay(result.envelope); + statuses.push({ + key, + status: decay.isExpired ? "expired" : decay.isStale ? "stale" : "ok", + description: manifest.description, + ...(decay.isStale && !decay.isExpired + ? { lifetimePercent: decay.lifetimePercent } + : {}), + }); + } - if (decay.isExpired) { - expiredCount++; - console.log( - ` ${c.red(SYMBOLS.warning)} ${c.bold(key)} ${c.bgRed(c.white(" EXPIRED "))} ${manifest.description ? c.dim(`— ${manifest.description}`) : ""}`, - ); - } else if (decay.isStale) { - staleCount++; - console.log( - ` ${c.yellow(SYMBOLS.warning)} ${c.bold(key)} ${c.yellow(`stale (${decay.lifetimePercent}%)`)} ${manifest.description ? c.dim(`— ${manifest.description}`) : ""}`, - ); + const present = statuses.filter((s) => s.status === "ok").length; + const missing = statuses.filter((s) => s.status === "missing").length; + const expiredCount = statuses.filter((s) => s.status === "expired").length; + const staleCount = statuses.filter((s) => s.status === "stale").length; + const total = Object.keys(config.secrets).length; + + if ( + emitJson(program, cmd, { + projectPath, + secrets: statuses, + summary: { total, present, missing, expired: expiredCount, stale: staleCount }, + ready: missing === 0, + }) + ) { + if (missing > 0) process.exitCode = 1; + return; + } + + console.log( + c.bold(`\n ${SYMBOLS.shield} Project secret manifest check\n`), + ); + + for (const s of statuses) { + const desc = s.description ? c.dim(`— ${s.description}`) : ""; + if (s.status === "missing") { + console.log(` ${c.red(SYMBOLS.cross)} ${c.bold(s.key)} ${c.red("MISSING")} ${desc}`); + } else if (s.status === "optional-not-set") { + console.log(` ${c.dim(SYMBOLS.cross)} ${c.bold(s.key)} ${c.dim("optional, not set")} ${desc}`); + } else if (s.status === "expired") { + console.log(` ${c.red(SYMBOLS.warning)} ${c.bold(s.key)} ${c.bgRed(c.white(" EXPIRED "))} ${desc}`); + } else if (s.status === "stale") { + console.log(` ${c.yellow(SYMBOLS.warning)} ${c.bold(s.key)} ${c.yellow(`stale (${s.lifetimePercent}%)`)} ${desc}`); } else { - present++; - console.log( - ` ${c.green(SYMBOLS.check)} ${c.bold(key)} ${c.green("OK")} ${manifest.description ? c.dim(`— ${manifest.description}`) : ""}`, - ); + console.log(` ${c.green(SYMBOLS.check)} ${c.bold(s.key)} ${c.green("OK")} ${desc}`); } } - const total = Object.keys(config.secrets).length; console.log( `\n ${c.bold(`${total} declared`)} ${c.green(`${present} present`)} ${c.yellow(`${staleCount} stale`)} ${c.red(`${expiredCount} expired`)} ${c.red(`${missing} missing`)}`, ); @@ -192,11 +216,14 @@ export function registerProjectCommands(program: Command): void { .command("env") .description("Show detected environment (wavefunction collapse context)") .option("--project-path ", "Project path for detection") + .option("--json", "Output as JSON") .action((cmd) => { const result = collapseEnvironment({ projectPath: cmd.projectPath ?? process.cwd(), }); + if (emitJson(program, cmd, { environment: result })) return; + if (result) { console.log( `${SYMBOLS.zap} ${c.bold("Collapsed environment:")} ${envBadge(result.env)} ${c.dim(`(source: ${result.source})`)}`, @@ -218,10 +245,21 @@ export function registerProjectCommands(program: Command): void { .option("--project-path ", "Project path (defaults to cwd)") .option("-o, --output ", "Output file path (defaults to stdout)") .option("-e, --env ", "Force environment for superposition collapse") - .action((cmd) => { + .option("-y, --yes", "Overwrite an existing output file without asking") + .action(async (cmd) => { const projectPath = cmd.projectPath ?? process.cwd(); const config = readProjectConfig(projectPath); + if ( + cmd.output && + existsSync(cmd.output) && + !(await confirm(`Overwrite existing ${cmd.output}?`, { assumeYes: cmd.yes })) + ) { + console.error(c.dim("Aborted.")); + process.exitCode = 1; + return; + } + if (!config?.secrets || Object.keys(config.secrets).length === 0) { console.error( c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`), @@ -304,6 +342,7 @@ export function registerProjectCommands(program: Command): void { .option("-g, --global", "Global scope") .option("-p, --project", "Project scope") .option("--project-path ", "Explicit project path") + .option("-y, --yes", "Skip confirmation when overwriting existing secrets") .action(async (name: string, cmd) => { const opts = buildOpts(cmd); const prefix = name.toUpperCase().replace(/[^A-Z0-9]/g, "_"); @@ -319,6 +358,21 @@ export function registerProjectCommands(program: Command): void { keyNames = [`${prefix}_API_KEY`, `${prefix}_API_SECRET`]; } + // The wizard generates fresh random values — overwriting an existing + // secret replaces a real credential with a placeholder. Ask first. + const existing = keyNames.filter((k) => hasSecret(k, opts)); + if ( + existing.length > 0 && + !(await confirm( + `${existing.length} secret(s) already exist and will be overwritten with generated values: ${existing.join(", ")}. Continue?`, + { assumeYes: cmd.yes }, + )) + ) { + console.error(c.dim("Aborted.")); + process.exitCode = 1; + return; + } + console.log(`\n${SYMBOLS.zap} ${c.bold(`Setting up service: ${name}`)}\n`); for (const key of keyNames) { diff --git a/src/cli/commands/quantum.ts b/src/cli/commands/quantum.ts index 50ec27c..7b51924 100644 --- a/src/cli/commands/quantum.ts +++ b/src/cli/commands/quantum.ts @@ -2,6 +2,7 @@ import type { Command } from "commander"; import { getSecret, setSecret, + hasSecret, listSecrets, entangleSecrets, disentangleSecrets, @@ -19,8 +20,9 @@ import { tunnelList, } from "../../core/tunnel.js"; import { teleportPack, teleportUnpack } from "../../core/teleport.js"; -import { promptSecret } from "../../utils/prompt.js"; +import { promptSecret, confirm } from "../../utils/prompt.js"; import { c, SYMBOLS } from "../../utils/colors.js"; +import { emitJson } from "../helpers.js"; import { buildOpts } from "../options.js"; export function registerQuantumCommands(program: Command): void { @@ -175,8 +177,10 @@ export function registerQuantumCommands(program: Command): void { .command("list") .alias("ls") .description("List active tunnels") - .action(() => { + .option("--json", "Output as JSON") + .action((cmd) => { const tunnels = tunnelList(); + if (emitJson(program, cmd, { tunnels })) return; if (tunnels.length === 0) { console.log(c.dim("No active tunnels")); return; @@ -259,6 +263,7 @@ export function registerQuantumCommands(program: Command): void { .option("-p, --project", "Import to project scope") .option("--project-path ", "Explicit project path") .option("--dry-run", "Show what would be imported without saving") + .option("-y, --yes", "Overwrite existing secrets without asking") .action(async (bundle: string | undefined, cmd) => { if (!bundle) { const chunks: Buffer[] = []; @@ -272,29 +277,11 @@ export function registerQuantumCommands(program: Command): void { `${SYMBOLS.lock} Enter passphrase for decryption: `, ); + // Only the decrypt itself maps to "wrong passphrase" — errors from the + // confirm/import steps below must not be swallowed by this catch. + let payload: ReturnType; try { - const payload = teleportUnpack(bundle, passphrase); - - if (cmd.dryRun) { - console.log( - `\n${SYMBOLS.package} ${c.bold("Would import")} (${payload.secrets.length} secrets):\n`, - ); - for (const s of payload.secrets) { - console.log( - ` ${SYMBOLS.key} ${c.bold(s.key)} ${c.dim(`[${s.scope ?? "global"}]`)}`, - ); - } - return; - } - - const opts = buildOpts(cmd); - for (const s of payload.secrets) { - setSecret(s.key, s.value, opts); - } - - console.log( - `${SYMBOLS.check} ${c.green("imported")} ${payload.secrets.length} secret(s) from teleport bundle`, - ); + payload = teleportUnpack(bundle, passphrase); } catch { console.error( c.red( @@ -303,5 +290,42 @@ export function registerQuantumCommands(program: Command): void { ); process.exit(1); } + + if (cmd.dryRun) { + console.log( + `\n${SYMBOLS.package} ${c.bold("Would import")} (${payload.secrets.length} secrets):\n`, + ); + for (const s of payload.secrets) { + console.log( + ` ${SYMBOLS.key} ${c.bold(s.key)} ${c.dim(`[${s.scope ?? "global"}]`)}`, + ); + } + return; + } + + const opts = buildOpts(cmd); + + const existing = payload.secrets + .map((s) => s.key) + .filter((k) => hasSecret(k, opts)); + if ( + existing.length > 0 && + !(await confirm( + `${existing.length} existing secret(s) will be overwritten: ${existing.join(", ")}. Continue?`, + { assumeYes: cmd.yes }, + )) + ) { + console.error(c.dim("Aborted.")); + process.exitCode = 1; + return; + } + + for (const s of payload.secrets) { + setSecret(s.key, s.value, opts); + } + + console.log( + `${SYMBOLS.check} ${c.green("imported")} ${payload.secrets.length} secret(s) from teleport bundle`, + ); }); } diff --git a/src/cli/commands/secrets.ts b/src/cli/commands/secrets.ts index dc924ff..2f30a8a 100644 --- a/src/cli/commands/secrets.ts +++ b/src/cli/commands/secrets.ts @@ -3,6 +3,7 @@ import { getSecret, setSecret, deleteSecret, + hasSecret, listSecrets, exportSecrets, getEnvelope, @@ -11,7 +12,7 @@ import { import type { Scope } from "../../core/scope.js"; import { checkDecay } from "../../core/envelope.js"; import { importDotenv } from "../../core/import.js"; -import { promptSecret } from "../../utils/prompt.js"; +import { promptSecret, confirm } from "../../utils/prompt.js"; import { c, scopeColor, @@ -19,7 +20,7 @@ import { envBadge, SYMBOLS, } from "../../utils/colors.js"; -import { safeStr, safeNum, safeArr, wantsJsonOutput } from "../helpers.js"; +import { safeStr, safeNum, safeArr, wantsJsonOutput, emitJson } from "../helpers.js"; import { buildOpts } from "../options.js"; import { filterSecretsByKeyGlob } from "../../services/list-secrets-filter.js"; @@ -143,6 +144,31 @@ export function registerSecretsCommands(program: Command): void { process.stdout.write(JSON.stringify(payload) + "\n"); }); + program + .command("has ") + .description( + "Check whether a secret exists (exit 0 if present, 1 if not) — decay-aware", + ) + .option("-g, --global", "Look only in global scope") + .option("-p, --project", "Look only in project scope") + .option("--team ", "Look only in team scope") + .option("--org ", "Look only in org scope") + .option("--project-path ", "Explicit project path") + .option("-q, --quiet", "No output, exit code only (for scripts)") + .action((key: string, cmd) => { + const opts = buildOpts(cmd); + const exists = hasSecret(key, opts); + + if (!cmd.quiet) { + if (wantsJsonOutput(program, cmd)) { + console.log(JSON.stringify({ ok: true, data: { key, exists } })); + } else { + console.log(exists ? "true" : "false"); + } + } + if (!exists) process.exitCode = 1; + }); + program .command("delete ") .alias("rm") @@ -150,16 +176,26 @@ export function registerSecretsCommands(program: Command): void { .option("-g, --global", "Delete from global scope only") .option("-p, --project", "Delete from project scope only") .option("--project-path ", "Explicit project path") - .action((key: string, cmd) => { + .option("-y, --yes", "Skip the confirmation prompt") + .option("--json", "Output as JSON") + .action(async (key: string, cmd) => { const opts = buildOpts(cmd); + + if (!(await confirm(`Delete secret ${key}?`, { assumeYes: cmd.yes }))) { + console.error(c.dim("Aborted.")); + process.exitCode = 1; + return; + } + const deleted = deleteSecret(key, opts); - if (deleted) { - console.log(`${SYMBOLS.check} ${c.green("deleted")} ${c.bold(key)}`); - } else { + if (!deleted) { console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`)); process.exit(1); } + + if (emitJson(program, cmd, { key, deleted: true })) return; + console.log(`${SYMBOLS.check} ${c.green("deleted")} ${c.bold(key)}`); }); program @@ -176,6 +212,7 @@ export function registerSecretsCommands(program: Command): void { .option("--expired", "Show only expired secrets") .option("--stale", "Show only stale secrets (75%+ decay)") .option("-f, --filter ", "Glob pattern on key name") + .option("--json", "Output as JSON") .action((cmd) => { const opts = buildOpts(cmd); let entries = listSecrets(opts); @@ -197,6 +234,27 @@ export function registerSecretsCommands(program: Command): void { entries = filterSecretsByKeyGlob(entries, cmd.filter); } + if ( + emitJson( + program, + cmd, + entries.map((e) => ({ + key: safeStr(e.key), + scope: safeStr(e.scope), + type: e.envelope?.states ? "superposition" : "collapsed", + environments: e.envelope?.states + ? Object.keys(e.envelope.states).map(safeStr) + : undefined, + tags: safeArr(e.envelope?.meta.tags), + accessCount: safeNum(e.envelope?.meta.accessCount), + entangledCount: safeNum(e.envelope?.meta.entangled?.length), + decay: e.decay ?? null, + })), + ) + ) { + return; + } + if (entries.length === 0) { console.log(c.dim("No secrets found")); return; @@ -261,6 +319,7 @@ export function registerSecretsCommands(program: Command): void { .option("-g, --global", "Inspect global scope only") .option("-p, --project", "Inspect project scope only") .option("--project-path ", "Explicit project path") + .option("--json", "Output as JSON") .action((key: string, cmd) => { const opts = buildOpts(cmd); const result = getEnvelope(key, opts); @@ -273,6 +332,35 @@ export function registerSecretsCommands(program: Command): void { const { envelope, scope } = result; const decay = checkDecay(envelope); + if ( + emitJson(program, cmd, { + key: safeStr(key), + scope: safeStr(scope), + type: envelope.states ? "superposition" : "collapsed", + environments: envelope.states + ? Object.keys(envelope.states).map(safeStr) + : undefined, + defaultEnv: envelope.defaultEnv ? safeStr(envelope.defaultEnv) : undefined, + createdAt: safeStr(envelope.meta.createdAt), + updatedAt: safeStr(envelope.meta.updatedAt), + accessCount: safeNum(envelope.meta.accessCount), + lastAccessedAt: envelope.meta.lastAccessedAt + ? safeStr(envelope.meta.lastAccessedAt) + : undefined, + description: envelope.meta.description + ? safeStr(envelope.meta.description) + : undefined, + tags: safeArr(envelope.meta.tags), + entangled: (envelope.meta.entangled ?? []).map((l) => ({ + service: safeStr(l.service), + key: safeStr(l.key), + })), + decay, + }) + ) { + return; + } + const safeScope = safeStr(scope) as Scope; const createdAt = safeStr(envelope.meta.createdAt); const updatedAt = safeStr(envelope.meta.updatedAt); @@ -372,6 +460,7 @@ export function registerSecretsCommands(program: Command): void { .option("-e, --env ", "Environment context") .option("--skip-existing", "Skip keys that already exist") .option("--dry-run", "Preview what would be imported without saving") + .option("--json", "Output as JSON") .action((file: string, cmd) => { const opts = buildOpts(cmd); @@ -383,6 +472,10 @@ export function registerSecretsCommands(program: Command): void { dryRun: cmd.dryRun, }); + if (emitJson(program, cmd, { file, dryRun: !!cmd.dryRun, ...result })) { + return; + } + if (cmd.dryRun) { console.log( `\n ${SYMBOLS.package} ${c.bold("Dry run")} — would import ${result.imported.length} of ${result.total} secrets:\n`, diff --git a/src/cli/commands/security.ts b/src/cli/commands/security.ts index e5c7333..7539ebb 100644 --- a/src/cli/commands/security.ts +++ b/src/cli/commands/security.ts @@ -6,7 +6,7 @@ import { } from "../../core/approval.js"; import { getPolicySummary } from "../../core/policy.js"; import { c, SYMBOLS } from "../../utils/colors.js"; -import { wantsJsonOutput } from "../helpers.js"; +import { wantsJsonOutput, emitJson } from "../helpers.js"; import { buildOpts } from "../options.js"; export function registerSecurityCommands(program: Command): void { @@ -79,8 +79,10 @@ export function registerSecurityCommands(program: Command): void { program .command("approvals") .description("List all approval tokens with verification status") - .action(() => { + .option("--json", "Output as JSON") + .action((cmd) => { const approvals = listApprovals(); + if (emitJson(program, cmd, { approvals })) return; if (approvals.length === 0) { console.log(c.dim(" No approvals found")); return; diff --git a/src/cli/commands/tooling.ts b/src/cli/commands/tooling.ts index 4a76d63..873d3e1 100644 --- a/src/cli/commands/tooling.ts +++ b/src/cli/commands/tooling.ts @@ -3,6 +3,7 @@ import { execCommand } from "../../core/exec.js"; import { scanCodebase } from "../../core/scan.js"; import { lintFiles } from "../../core/linter.js"; import { c, SYMBOLS } from "../../utils/colors.js"; +import { emitJson, wantsJsonOutput } from "../helpers.js"; import { buildOpts } from "../options.js"; export function registerToolingCommands(program: Command): void { @@ -56,15 +57,25 @@ export function registerToolingCommands(program: Command): void { .option("-g, --global", "Store fixed secrets in global scope") .option("-p, --project", "Store fixed secrets in project scope") .option("--project-path ", "Explicit project path") + .option("--json", "Output as JSON") .action((dir: string | undefined, cmd) => { const targetDir = dir ?? process.cwd(); const fixMode = cmd.fix === true; - console.log( - `\n ${SYMBOLS.eye} Scanning ${c.bold(targetDir)} for secrets...${fixMode ? c.yellow(" [--fix mode]") : ""}\n`, - ); + const jsonMode = wantsJsonOutput(program, cmd); + if (!jsonMode) { + console.log( + `\n ${SYMBOLS.eye} Scanning ${c.bold(targetDir)} for secrets...${fixMode ? c.yellow(" [--fix mode]") : ""}\n`, + ); + } const results = scanCodebase(targetDir); + if (jsonMode && !fixMode) { + emitJson(program, cmd, { dir: targetDir, findings: results }); + if (results.length > 0) process.exitCode = 1; + return; + } + if (results.length === 0) { console.log( ` ${c.green(SYMBOLS.check)} No hardcoded secrets found. Awesome!\n`, @@ -85,6 +96,14 @@ export function registerToolingCommands(program: Command): void { projectPath: opts.projectPath, }); const fixedCount = lintResults.filter((r) => r.fixed).length; + if (jsonMode) { + emitJson(program, cmd, { + dir: targetDir, + findings: results, + fixed: fixedCount, + }); + return; + } console.log( ` ${c.green(SYMBOLS.check)} Fixed ${fixedCount} secrets — replaced with process.env references and stored in q-ring.\n`, ); @@ -118,6 +137,7 @@ export function registerToolingCommands(program: Command): void { .option("-g, --global", "Store fixed secrets in global scope") .option("-p, --project", "Store fixed secrets in project scope") .option("--project-path ", "Explicit project path") + .option("--json", "Output as JSON") .action((files: string[], cmd) => { const opts = buildOpts(cmd); const results = lintFiles(files, { @@ -126,6 +146,11 @@ export function registerToolingCommands(program: Command): void { projectPath: opts.projectPath, }); + if (emitJson(program, cmd, { files, findings: results })) { + if (results.some((r) => !r.fixed)) process.exitCode = 1; + return; + } + if (results.length === 0) { console.log( `\n ${c.green(SYMBOLS.check)} No hardcoded secrets found in ${files.length} file(s).\n`, diff --git a/src/cli/commands/validation.ts b/src/cli/commands/validation.ts index 41111e5..1da181d 100644 --- a/src/cli/commands/validation.ts +++ b/src/cli/commands/validation.ts @@ -13,7 +13,7 @@ import { registry as providerRegistry, } from "../../core/validate.js"; import { c, SYMBOLS } from "../../utils/colors.js"; -import { wantsJsonOutput } from "../helpers.js"; +import { wantsJsonOutput, emitJson } from "../helpers.js"; import { buildOpts } from "../options.js"; export function registerValidationCommands(program: Command): void { @@ -36,8 +36,12 @@ export function registerValidationCommands(program: Command): void { "Only validate manifest-declared secrets (with --all)", ) .option("--list-providers", "List all available providers") + .option("--json", "Output as JSON") .action(async (key: string | undefined, cmd) => { if (cmd.listProviders) { + if (emitJson(program, cmd, { providers: providerRegistry.listProviders() })) { + return; + } console.log( c.bold(`\n ${SYMBOLS.shield} Available validation providers\n`), ); @@ -72,10 +76,20 @@ export function registerValidationCommands(program: Command): void { } } - console.log(c.bold(`\n ${SYMBOLS.shield} Validating secrets\n`)); + const jsonMode = wantsJsonOutput(program, cmd); + if (!jsonMode) { + console.log(c.bold(`\n ${SYMBOLS.shield} Validating secrets\n`)); + } let validated = 0; let skipped = 0; + const jsonResults: Array<{ + key: string; + status: string; + provider: string; + latencyMs: number; + message?: string; + }> = []; for (const entry of entries) { const value = getSecret(entry.key, { ...opts, scope: entry.scope }); @@ -93,6 +107,16 @@ export function registerValidationCommands(program: Command): void { } validated++; + if (jsonMode) { + jsonResults.push({ + key: entry.key, + status: result.status, + provider: result.provider, + latencyMs: result.latencyMs, + message: result.status !== "valid" ? result.message : undefined, + }); + continue; + } const icon = result.status === "valid" ? c.green(SYMBOLS.check) @@ -111,6 +135,11 @@ export function registerValidationCommands(program: Command): void { ); } + if (jsonMode) { + emitJson(program, cmd, { results: jsonResults, validated, skipped }); + return; + } + console.log( `\n ${c.dim(`${validated} validated, ${skipped} skipped (no provider)`)}\n`, ); @@ -127,6 +156,18 @@ export function registerValidationCommands(program: Command): void { const provHint = envelope?.envelope.meta.provider ?? cmd.provider; const result = await validateSecret(value, { provider: provHint }); + if ( + emitJson(program, cmd, { + key, + status: result.status, + provider: result.provider, + latencyMs: result.latencyMs, + message: result.message, + }) + ) { + return; + } + const icon = result.status === "valid" ? c.green(SYMBOLS.check) @@ -154,6 +195,7 @@ export function registerValidationCommands(program: Command): void { .option("-p, --project", "Project scope") .option("--project-path ", "Explicit project path") .option("--provider ", "Force a specific provider") + .option("--json", "Output as JSON") .action(async (key: string, cmd) => { const opts = buildOpts(cmd); const value = getSecret(key, opts); @@ -169,6 +211,20 @@ export function registerValidationCommands(program: Command): void { ...opts, scope: opts.scope ?? "global", }); + } + + if ( + emitJson(program, cmd, { + key, + rotated: !!(result.rotated && result.newValue), + provider: result.provider, + message: result.message, + }) + ) { + return; + } + + if (result.rotated && result.newValue) { console.log( `${SYMBOLS.check} ${c.green("Rotated")} ${c.bold(key)} via ${result.provider}`, ); diff --git a/src/cli/helpers.ts b/src/cli/helpers.ts index 1f54295..d9d803b 100644 --- a/src/cli/helpers.ts +++ b/src/cli/helpers.ts @@ -8,6 +8,16 @@ export function wantsJsonOutput(program: Command, cmd: unknown): boolean { return !!g.json; } +/** + * Emit `{ ok: true, data }` as pretty-printed JSON when `--json` is in + * effect. Returns true if JSON was emitted (caller should skip human output). + */ +export function emitJson(program: Command, cmd: unknown, data: unknown): boolean { + if (!wantsJsonOutput(program, cmd)) return false; + process.stdout.write(JSON.stringify({ ok: true, data }, null, 2) + "\n"); + return true; +} + /** * Break the CodeQL taint chain from getPassword → console.log. * Copies a string value so static analysis no longer considers it diff --git a/src/core/dashboard-client.ts b/src/core/dashboard-client.ts index 278a87a..a73ee56 100644 --- a/src/core/dashboard-client.ts +++ b/src/core/dashboard-client.ts @@ -2,4 +2,4 @@ // Bundled status-dashboard client: Preact 10.29.2 + htm 3.1.1 + app. // Source: src/dashboard-client/main.js. Regenerate with: pnpm run build:dashboard -export const DASHBOARD_CLIENT = "\"use strict\";(()=>{var G,g,we,et,N,ge,Ce,Se,se,W,L,Me,le,ae,oe,tt,O={},Y=[],nt=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Q=Array.isArray;function R(t,e){for(var n in e)t[n]=e[n];return t}function re(t){t&&t.parentNode&&t.parentNode.removeChild(t)}function ce(t,e,n){var s,o,a,l={};for(a in e)a==\"key\"?s=e[a]:a==\"ref\"?o=e[a]:l[a]=e[a];if(arguments.length>2&&(l.children=arguments.length>3?G.call(arguments,2):n),typeof t==\"function\"&&t.defaultProps!=null)for(a in t.defaultProps)l[a]===void 0&&(l[a]=t.defaultProps[a]);return z(t,l,s,o,null)}function z(t,e,n,s,o){var a={type:t,props:e,key:n,ref:s,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o==null?++we:o,__i:-1,__u:0};return o==null&&g.vnode!=null&&g.vnode(a),a}function X(t){return t.children}function K(t,e){this.props=t,this.context=e}function D(t,e){if(e==null)return t.__?D(t.__,t.__i+1):null;for(var n;ee&&N.sort(Se),t=N.shift(),e=N.length,st(t)}finally{N.length=J.__r=0}}function Ee(t,e,n,s,o,a,l,d,_,c,i){var r,p,v,f,C,k,y,h=s&&s.__k||Y,$=e.length;for(_=at(n,e,h,_,$),r=0;r<$;r++)(v=n.__k[r])!=null&&(p=v.__i!=-1&&h[v.__i]||O,v.__i=r,k=de(t,v,p,o,a,l,d,_,c,i),f=v.__e,v.ref&&p.ref!=v.ref&&(p.ref&&ue(p.ref,null,v),i.push(v.ref,v.__c||f,v)),C==null&&f!=null&&(C=f),(y=!!(4&v.__u))||p.__k===v.__k?(_=Pe(v,_,t,y),y&&p.__e&&(p.__e=null)):typeof v.type==\"function\"&&k!==void 0?_=k:f&&(_=f.nextSibling),v.__u&=-7);return n.__e=C,_}function at(t,e,n,s,o){var a,l,d,_,c,i=n.length,r=i,p=0;for(t.__k=new Array(o),a=0;a0?l=t.__k[a]=z(l.type,l.props,l.key,l.ref?l.ref:null,l.__v):t.__k[a]=l,_=a+p,l.__=t,l.__b=t.__b+1,d=null,(c=l.__i=ot(l,n,_,r))!=-1&&(r--,(d=n[c])&&(d.__u|=2)),d==null||d.__v==null?(c==-1&&(o>i?p--:o_?p--:p++,l.__u|=4))):t.__k[a]=null;if(r)for(a=0;a(i?1:0)){for(o=n-1,a=n+1;o>=0||a=0?o--:a++])!=null&&(2&c.__u)==0&&d==c.key&&_==c.type)return l}return-1}function be(t,e,n){e[0]==\"-\"?t.setProperty(e,n==null?\"\":n):t[e]=n==null?\"\":typeof n!=\"number\"||nt.test(e)?n:n+\"px\"}function V(t,e,n,s,o){var a,l;e:if(e==\"style\")if(typeof n==\"string\")t.style.cssText=n;else{if(typeof s==\"string\"&&(t.style.cssText=s=\"\"),s)for(e in s)n&&e in n||be(t.style,e,\"\");if(n)for(e in n)s&&n[e]==s[e]||be(t.style,e,n[e])}else if(e[0]==\"o\"&&e[1]==\"n\")a=e!=(e=e.replace(Me,\"$1\")),l=e.toLowerCase(),e=l in t||e==\"onFocusOut\"||e==\"onFocusIn\"?l.slice(2):e.slice(2),t.l||(t.l={}),t.l[e+a]=n,n?s?n[L]=s[L]:(n[L]=le,t.addEventListener(e,a?oe:ae,a)):t.removeEventListener(e,a?oe:ae,a);else{if(o==\"http://www.w3.org/2000/svg\")e=e.replace(/xlink(H|:h)/,\"h\").replace(/sName$/,\"s\");else if(e!=\"width\"&&e!=\"height\"&&e!=\"href\"&&e!=\"list\"&&e!=\"form\"&&e!=\"tabIndex\"&&e!=\"download\"&&e!=\"rowSpan\"&&e!=\"colSpan\"&&e!=\"role\"&&e!=\"popover\"&&e in t)try{t[e]=n==null?\"\":n;break e}catch{}typeof n==\"function\"||(n==null||n===!1&&e[4]!=\"-\"?t.removeAttribute(e):t.setAttribute(e,e==\"popover\"&&n==1?\"\":n))}}function xe(t){return function(e){if(this.l){var n=this.l[e.type+t];if(e[W]==null)e[W]=le++;else if(e[W]0?t:Q(t)?t.map(Te):t.constructor!==void 0?null:R({},t)}function it(t,e,n,s,o,a,l,d,_){var c,i,r,p,v,f,C,k=n.props||O,y=e.props,h=e.type;if(h==\"svg\"?o=\"http://www.w3.org/2000/svg\":h==\"math\"?o=\"http://www.w3.org/1998/Math/MathML\":o||(o=\"http://www.w3.org/1999/xhtml\"),a!=null){for(c=0;c=n.__.length&&n.__.push({}),n.__[t]}function q(t){return I=1,rt(ze,t)}function rt(t,e,n){var s=ve(U++,2);if(s.t=t,!s.__c&&(s.__=[n?n(e):ze(void 0,e),function(d){var _=s.__N?s.__N[0]:s.__[0],c=s.t(_,d);_!==c&&(s.__N=[c,s.__[1]],s.__c.setState({}))}],s.__c=x,!x.__f)){var o=function(d,_,c){if(!s.__c.__H)return!0;var i=s.__c.__H.__.filter(function(p){return p.__c});if(i.every(function(p){return!p.__N}))return!a||a.call(this,d,_,c);var r=s.__c.props!==d;return i.some(function(p){if(p.__N){var v=p.__[0];p.__=p.__N,p.__N=void 0,v!==p.__[0]&&(r=!0)}}),a&&a.call(this,d,_,c)||r};x.__f=!0;var a=x.shouldComponentUpdate,l=x.componentWillUpdate;x.componentWillUpdate=function(d,_,c){if(this.__e){var i=a;a=void 0,o(d,_,c),a=i}l&&l.call(this,d,_,c)},x.shouldComponentUpdate=o}return s.__N||s.__}function ee(t,e){var n=ve(U++,3);!w.__s&&We(n.__H,e)&&(n.__=t,n.u=e,x.__H.__h.push(n))}function fe(t){return I=5,te(function(){return{current:t}},[])}function te(t,e){var n=ve(U++,7);return We(n.__H,e)&&(n.__=t(),n.__H=e,n.__h=t),n.__}function ye(t,e){return I=8,te(function(){return t},e)}function ct(){for(var t;t=Ve.shift();){var e=t.__H;if(t.__P&&e)try{e.__h.some(Z),e.__h.some(pe),e.__h=[]}catch(n){e.__h=[],w.__e(n,t.__v)}}}w.__b=function(t){x=null,De&&De(t)},w.__=function(t,e){t&&e.__k&&e.__k.__m&&(t.__m=e.__k.__m),Fe&&Fe(t,e)},w.__r=function(t){je&&je(t),U=0;var e=(x=t.__c).__H;e&&(_e===x?(e.__h=[],x.__h=[],e.__.some(function(n){n.__N&&(n.__=n.__N),n.u=n.__N=void 0})):(e.__h.some(Z),e.__h.some(pe),e.__h=[],U=0)),_e=x},w.diffed=function(t){Le&&Le(t);var e=t.__c;e&&e.__H&&(e.__H.__h.length&&(Ve.push(e)!==1&&He===w.requestAnimationFrame||((He=w.requestAnimationFrame)||dt)(ct)),e.__H.__.some(function(n){n.u&&(n.__H=n.u),n.u=void 0})),_e=x=null},w.__c=function(t,e){e.some(function(n){try{n.__h.some(Z),n.__h=n.__h.filter(function(s){return!s.__||pe(s)})}catch(s){e.some(function(o){o.__h&&(o.__h=[])}),e=[],w.__e(s,n.__v)}}),Ue&&Ue(t,e)},w.unmount=function(t){Ie&&Ie(t);var e,n=t.__c;n&&n.__H&&(n.__H.__.some(function(s){try{Z(s)}catch(o){e=o}}),n.__H=void 0,e&&w.__e(e,n.__v))};var Be=typeof requestAnimationFrame==\"function\";function dt(t){var e,n=function(){clearTimeout(s),Be&&cancelAnimationFrame(e),setTimeout(t)},s=setTimeout(n,35);Be&&(e=requestAnimationFrame(n))}function Z(t){var e=x,n=t.__c;typeof n==\"function\"&&(t.__c=void 0,n()),x=e}function pe(t){var e=x;t.__c=t.__(),x=e}function We(t,e){return!t||t.length!==e.length||e.some(function(n,s){return n!==t[s]})}function ze(t,e){return typeof e==\"function\"?e(t):e}var Oe=function(t,e,n,s){var o;e[0]=0;for(var a=1;a=5&&((l||!p&&a===5)&&(_.push(a,0,l,o),a=6),p&&(_.push(a,p,0,o),a=6)),l=\"\"},i=0;i\"?(a=1,l=\"\"):l=s+l[0]:d?s===d?d=\"\":l+=s:s==='\"'||s===\"'\"?d=s:s===\">\"?(c(),a=1):a&&(s===\"=\"?(a=5,o=l,l=\"\"):s===\"/\"&&(a<5||n[i][r+1]===\">\")?(c(),a===3&&(_=_[0]),a=_,(_=_[0]).push(2,0,a),a=0):s===\" \"||s===\"\t\"||s===`\n`||s===\"\\r\"?(c(),a=2):l+=s),a===3&&l===\"!--\"&&(a=4,_=_[0])}return c(),_})(t)),e),arguments,[])).length>1?e:e[0]}var u=Ye.bind(ce),Je=window.__QRING__&&window.__QRING__.token||\"\",he=Je?\"?token=\"+encodeURIComponent(Je):\"\",A=t=>''+t+\"\",m={health:A(''),environment:A(''),decay:A(''),superposition:A(''),entangle:A(''),tunnel:A(''),anomaly:A(''),audit:A(''),key:A(''),hook:A(''),approve:A(''),manifest:A(''),policy:A(''),memory:A(''),folder:'',search:'',lock:''};function me(t){if(!t)return\"env-default\";let e=t.toLowerCase();return e===\"prod\"||e===\"production\"?\"env-prod\":e===\"staging\"||e===\"stage\"?\"env-staging\":e===\"dev\"||e===\"development\"?\"env-dev\":e===\"test\"||e===\"testing\"?\"env-test\":\"env-default\"}function Qe(t,e){return e||t>=90?\"var(--danger)\":t>=75?\"var(--warning)\":\"var(--accent)\"}function Xe(t){let e=new Date(t);return String(e.getHours()).padStart(2,\"0\")+\":\"+String(e.getMinutes()).padStart(2,\"0\")+\":\"+String(e.getSeconds()).padStart(2,\"0\")}function Ze(t){if(!t)return\"\\u2014\";let e=Math.floor((Date.now()-new Date(t).getTime())/1e3);return isNaN(e)?\"\\u2014\":e<5?\"just now\":e<60?e+\"s ago\":e<3600?Math.floor(e/60)+\"m ago\":e<86400?Math.floor(e/3600)+\"h ago\":Math.floor(e/86400)+\"d ago\"}function $e(t){return t==null?\"\\u2014\":t<0?\"expired\":t<60?t+\"s\":t<3600?Math.floor(t/60)+\"m\":t<86400?Math.floor(t/3600)+\"h\":Math.floor(t/86400)+\"d\"}var j=({svg:t})=>u``;function S({icon:t,title:e,aside:n,asideStyle:s}){return u`
\n <${j} svg=${t} />${e}\n ${n!=null?u`${n}`:null}\n
`}function M({delay:t=0,wide:e=!1,style:n=\"\",children:s}){let o=\"card\"+(e?\" grid-wide\":\"\"),a=\"animation-delay:\"+t+\"ms\"+(n?\";\"+n:\"\");return u`
${s}
`}function E({children:t,cta:e}){return u`
${t}${e?u`${e}`:null}
`}function Ge({snap:t,paused:e,connState:n,onPause:s,onRefresh:o,lastUpdate:a}){let l=e?\"paused\":n===\"live\"?\"live\":n===\"reconnecting\"?\"reconnecting\\u2026\":\"connecting\\u2026\",d=\"status-dot\"+(e?\" paused\":n===\"reconnecting\"?\" disconnected\":\"\");return u`\n
\n
\n

\n \n q-ring\n quantum status\n

\n \n \n ${\" v\"+(t?t.version:\"\")}\n \n \n <${j} svg=${m.folder} /> ${t?t.projectPath:\"\"}\n \n
\n
\n ${a}\n \n \n \n \n JSON\n \n ${l}\n
\n
\n `}function ut({snap:t}){let e=t,n=e.auditMetrics||{total:0,byAction:{},topRead:[]},s=n.byAction.read||0,o=(n.byAction.write||0)+(n.byAction.delete||0)+(n.byAction.rotate||0),a=n.byAction.policy_deny||0,l=(e.hooks||[]).filter(i=>i.enabled).length,d=(e.approvals||[]).filter(i=>i.valid).length,_=(e.anomalies||[]).length,c=[{label:\"Secrets\",icon:m.key,value:e.health.total,sub:e.health.healthy+\" healthy \\xB7 \"+e.health.stale+\" stale \\xB7 \"+e.health.expired+\" expired\",cls:\"\"},{label:\"Environment\",icon:m.environment,value:e.environment?e.environment.env:\"none\",sub:e.environment?\"via \"+e.environment.source:\"no env detected\",cls:e.environment?\"\":\"dim\"},{label:\"Protected\",icon:m.lock,value:e.protectedCount,sub:\"require approval\",cls:e.protectedCount>0?\"warning\":\"dim\"},{label:\"Approvals\",icon:m.approve,value:d,sub:\"active grants\",cls:d>0?\"green\":\"dim\"},{label:\"Hooks\",icon:m.hook,value:l,sub:\"enabled / \"+(e.hooks||[]).length+\" total\",cls:l>0?\"\":\"dim\"},{label:\"Reads (24h)\",icon:m.audit,value:s,sub:o+\" writes \\xB7 \"+a+\" denied\",cls:\"\"},{label:\"Anomalies\",icon:m.anomaly,value:_,sub:_?\"investigate now\":\"all clear\",cls:_?\"danger\":\"green\"}];return u`
\n ${c.map(i=>u`
\n
<${j} svg=${i.icon} />${i.label}
\n
${i.value}
\n
${i.sub}
\n
`)}\n
`}function _t({snap:t,delay:e}){let n=t.health,s=n.total||1,o=42,a=2*Math.PI*o,l=[{v:n.healthy,c:\"var(--accent)\"},{v:n.stale,c:\"var(--warning)\"},{v:n.expired,c:\"var(--danger)\"},{v:n.noDecay,c:\"var(--text-dim)\"}],d=0,_=l.map((i,r)=>{let p=i.v/s*a,v=u``;return d+=p,v}),c=t.scopes||{global:0,project:0,team:0,org:0};return u`<${M} delay=${e}>\n <${S} icon=${m.health} title=\"Health Summary\" aside=${n.healthy+\"/\"+n.total+\" healthy\"} />\n
\n
\n ${_}\n
${n.total}secrets
\n
\n
\n
Healthy ${n.healthy}
\n
Stale ${n.stale}
\n
Expired ${n.expired}
\n
No decay ${n.noDecay}
\n
\n
\n
\n ${[\"global\",\"project\",\"team\",\"org\"].filter(i=>c[i]).map(i=>u`${c[i]}${i}`)}\n
\n `}function pt({snap:t,delay:e}){let n=t.environment;return u`<${M} delay=${e}>\n <${S} icon=${m.environment} title=\"Environment\" />\n ${n?u`
${n.env}detected via ${n.source}
`:u`<${E} cta=\"$ qring env --project-path .\">No environment detected.`}\n `}function vt({snap:t,delay:e}){let n=t.manifest;if(!n)return u`<${M} delay=${e}>\n <${S} icon=${m.manifest} title=\"Manifest\" />\n <${E} cta=${u`$ qring wizard `}>No .q-ring.json manifest in this project.\n `;let s=n.required-n.missing.length-n.expired.length-n.stale.length,o=n.required?Math.max(0,Math.round(s/n.required*100)):100,a=n.missing.length?\"var(--danger)\":n.expired.length||n.stale.length?\"var(--warning)\":\"var(--green)\",l=(_,c,i)=>u`
${_} (${c.length})
${c.map(r=>u`${r}`)}
`,d=n.missing.length||n.expired.length||n.stale.length;return u`<${M} delay=${e}>\n <${S} icon=${m.manifest} title=\"Manifest\" aside=${n.required+\" required / \"+n.declared+\" declared\"} />\n
\n
healthy
${o}%
\n
\n
\n ${n.missing.length?l(\"Missing required\",n.missing,\"miss\"):null}\n ${n.expired.length?l(\"Expired\",n.expired,\"exp\"):null}\n ${n.stale.length?l(\"Stale\",n.stale,\"exp\"):null}\n ${d?null:u`
All required keys present and healthy.
`}\n
\n `}function ft({snap:t,delay:e}){let n=t.policy||{counts:{}},s=n.counts||{};if(!(n.hasMcpPolicy||n.hasExecPolicy||n.hasSecretPolicy))return u`<${M} delay=${e}>\n <${S} icon=${m.policy} title=\"Policy\" />\n <${E} cta=${u`add policy to .q-ring.json`}>No governance policy declared.\n `;let a=({label:d,value:_})=>u`
${d}${_||\"\\u2014\"}
`,l=[n.hasMcpPolicy?\"mcp\":\"\",n.hasExecPolicy?\"exec\":\"\",n.hasSecretPolicy?\"secrets\":\"\"].filter(Boolean).join(\" \\xB7 \")||\"none\";return u`<${M} delay=${e}>\n <${S} icon=${m.policy} title=\"Policy\" aside=${l} />\n
\n <${a} label=\"MCP allow tools\" value=${s.allowTools} />\n <${a} label=\"MCP deny tools\" value=${s.denyTools} />\n <${a} label=\"Denied keys\" value=${s.deniedKeys} />\n <${a} label=\"Denied tags\" value=${s.deniedTags} />\n <${a} label=\"Exec allow\" value=${s.allowCommands} />\n <${a} label=\"Exec deny\" value=${s.denyCommands} />\n <${a} label=\"Tag → approval\" value=${s.requireApprovalForTags} />\n <${a} label=\"Tag → rotation\" value=${s.requireRotationFormatForTags} />\n ${n.maxTtlSeconds?u`<${a} label=\"Max TTL\" value=${$e(n.maxTtlSeconds)} />`:null}\n ${n.maxRuntimeSeconds?u`<${a} label=\"Max exec runtime\" value=${$e(n.maxRuntimeSeconds)} />`:null}\n
\n `}function yt({snap:t,delay:e}){let n=(t.secrets||[]).filter(s=>s.decay&&s.decay.timeRemaining).sort((s,o)=>(s.decay.secondsRemaining||0)-(o.decay.secondsRemaining||0));return u`<${M} delay=${e}>\n <${S} icon=${m.decay} title=\"Decay Timers\" aside=${n.length+\" tracked\"} />\n ${n.length?u`
${n.slice(0,12).map(s=>{let o=Math.min(s.decay.lifetimePercent,100),a=Qe(o,s.decay.isExpired),l=s.decay.isExpired?\"expired\":s.decay.timeRemaining||\"\";return u`
\n ${s.key}\n
\n ${l}\n
`})}
`:u`<${E} cta=\"$ qring set KEY --ttl 86400\">No secrets with decay configured.`}\n `}function ht({snap:t,delay:e}){let n=(t.secrets||[]).filter(s=>s.type===\"superposition\"&&s.environments&&s.environments.length);return u`<${M} delay=${e}>\n <${S} icon=${m.superposition} title=\"Superposition\" aside=${n.length+\" multi-env\"} />\n ${n.length?u`
${n.slice(0,14).map(s=>u`
\n ${s.key}\n ${(s.environments||[]).map(o=>u`${o}${o===s.defaultEnv?\" \\u2713\":\"\"}`)}\n
`)}
`:u`<${E} cta=\"$ qring set KEY --env prod\">No secrets in superposition.`}\n `}function $t({snap:t,delay:e}){let n=t.entanglements||[],s=new Set,o=n.filter(a=>{let l=[a.source.service,a.source.key,a.target.service,a.target.key].sort().join(\"|\");return s.has(l)?!1:(s.add(l),!0)});return u`<${M} delay=${e}>\n <${S} icon=${m.entangle} title=\"Entanglement\" aside=${o.length+\" pair\"+(o.length===1?\"\":\"s\")} />\n ${o.length?u`
${o.slice(0,12).map((a,l)=>u`
\n ${a.source.key}${a.target.key}\n
`)}
`:u`<${E} cta=\"$ qring entangle KEY_A KEY_B\">No entangled secrets.`}\n `}function mt({snap:t,delay:e}){let n=t.tunnels||[];return u`<${M} delay=${e}>\n <${S} icon=${m.tunnel} title=\"Quantum Tunnels\" aside=${n.length+\" live\"} />\n ${n.length?u`
${n.map(s=>{let o=s.expiresAt?Math.max(0,Math.floor((s.expiresAt-Date.now())/1e3)):null;return u`
${s.id}
\n reads: ${s.accessCount}${s.maxReads?\"/\"+s.maxReads:\"\"}\n ${o!==null?u`expires: ${o}s`:u`no expiry`}\n
`})}
`:u`<${E} cta=${u`$ qring tunnel create `}>No active tunnels.`}\n `}function gt({snap:t,delay:e}){let n=t.approvals||[];return u`<${M} delay=${e}>\n <${S} icon=${m.approve} title=\"Approvals\" aside=${n.filter(s=>s.valid).length+\" valid\"} />\n ${n.length?u`
${n.slice(0,8).map(s=>{let o=s.tampered?\"tampered\":s.secondsRemaining<300?\"expiring\":\"\";return u`
\n
${s.key}${s.tampered?\" \\u26A0 TAMPERED\":\"\"}${$e(s.secondsRemaining)} left
\n
${s.reason||\"(no reason)\"}
\n
scope:${s.scope}by:${s.grantedBy}
\n
`})}
`:u`<${E} cta=${'$ qring approve KEY --for 1800 --reason \"...\"'}>No active approvals.`}\n `}function kt({snap:t,delay:e}){let n=t.hooks||[];return u`<${M} delay=${e}>\n <${S} icon=${m.hook} title=\"Hooks\" aside=${n.filter(s=>s.enabled).length+\" enabled / \"+n.length} />\n ${n.length?u`
${n.slice(0,8).map(s=>u`
\n ${s.type}\n ${s.description||s.matchSummary}\n ${s.id}\n
`)}
`:u`<${E} cta=${'$ qring hook add --tag payments --action rotate --exec \"...\"'}>No hooks registered.`}\n `}function bt({snap:t,delay:e}){return u`<${M} delay=${e}>\n <${S} icon=${m.memory} title=\"Agent Memory\" />\n
\n ${t.memoryKeys||0}\n memory key${t.memoryKeys===1?\"\":\"s\"} encrypted at ~/.config/q-ring/agent-memory.enc\n
\n `}function xt({snap:t,delay:e}){let n=t.anomalies||[];if(!n.length)return u`<${M} delay=${e} wide=${!0}>\n <${S} icon=${m.anomaly} title=\"Anomaly Alerts\" aside=\"all clear\" asideStyle=\"color:var(--green)\" />\n <${E}>No anomalies detected — quantum field is stable.\n `;let s=o=>o===\"tampered\"?\"$ qring audit:verify\":o===\"burst\"?\"$ qring audit --key \\u2026 --action read\":o===\"unusual-hour\"?'$ qring audit --action read --since \"1am\"':\"$ qring audit --limit 100\";return u`<${M} delay=${e} wide=${!0}>\n <${S} icon=${m.anomaly} title=\"Anomaly Alerts\" aside=${n.length+\" active\"} asideStyle=\"color:var(--danger)\" />\n
${n.map((o,a)=>u`
\n
${o.type}
\n
${o.description}
\n
${s(o.type)}
\n
`)}
\n `}function wt(t,e){let n=e.trim().toLowerCase();return n?n===\"expired\"?t.filter(s=>s.decay&&s.decay.isExpired):n===\"stale\"?t.filter(s=>s.decay&&s.decay.isStale&&!s.decay.isExpired):n===\"protected\"?t.filter(s=>s.requiresApproval):t.filter(s=>[s.key,s.scope,s.type,s.provider||\"\",s.defaultEnv||\"\",(s.environments||[]).join(\" \"),(s.tags||[]).join(\" \")].join(\" \").toLowerCase().indexOf(n)!==-1):t.slice()}function Ct(t,e){let n=e.dir===\"asc\"?1:-1;return t.sort((s,o)=>{let a,l;switch(e.col){case\"key\":a=s.key,l=o.key;break;case\"scope\":a=s.scope,l=o.scope;break;case\"env\":a=s.defaultEnv||(s.environments||[])[0]||\"\",l=o.defaultEnv||(o.environments||[])[0]||\"\";break;case\"type\":a=s.type,l=o.type;break;case\"decay\":a=s.decay&&s.decay.secondsRemaining!=null?s.decay.secondsRemaining:1/0,l=o.decay&&o.decay.secondsRemaining!=null?o.decay.secondsRemaining:1/0;break;case\"tags\":a=(s.tags||[]).join(\",\"),l=(o.tags||[]).join(\",\");break;default:a=s.lastAccessedAt?new Date(s.lastAccessedAt).getTime():0,l=o.lastAccessedAt?new Date(o.lastAccessedAt).getTime():0}return al?1*n:0})}function St({snap:t,query:e,setQuery:n,sort:s,setSort:o,searchRef:a}){let l=t.secrets||[],d=te(()=>Ct(wt(l,e),s),[l,e,s]),_=[{id:\"key\",label:\"Key\"},{id:\"scope\",label:\"Scope\"},{id:\"env\",label:\"Env\"},{id:\"type\",label:\"Type\"},{id:\"decay\",label:\"Decay\"},{id:\"tags\",label:\"Tags\"},{id:\"lastAccessedAt\",label:\"Last read\"}],c=i=>{s.col===i?o({col:i,dir:s.dir===\"asc\"?\"desc\":\"asc\"}):o({col:i,dir:i===\"key\"?\"asc\":\"desc\"})};return u`
<${M} wide=${!0} style=\"margin-bottom:16px\">\n <${S} icon=${m.key} title=\"Secrets\" aside=${d.length+\" of \"+l.length} />\n
\n \n \n \n \n \n
\n
\n ${d.length?u`\n ${_.map(i=>u``)}\n ${d.map(i=>{let r=i.type===\"superposition\"?(i.environments||[]).map(f=>u`${f}${f===i.defaultEnv?\" \\u2713\":\"\"}`):u``,p=i.decay&&i.decay.timeRemaining?(()=>{let f=Math.min(i.decay.lifetimePercent,100),C=Qe(f,i.decay.isExpired);return u`${i.decay.isExpired?\"expired\":i.decay.timeRemaining}`})():u``,v=i.tags||[];return u`\n \n \n \n \n \n \n \n `})}\n
c(i.id)}>${i.label}${s.dir===\"asc\"?\"\\u25B2\":\"\\u25BC\"}
${i.key}${i.requiresApproval?u`<${j} svg=${m.lock} />`:null}${i.scope}${r}${i.type}${p}${v.slice(0,3).map(f=>u`${f}`)}${v.length>3?\"+\"+(v.length-3):\"\"}${Ze(i.lastAccessedAt)}
`:u`
No secrets match the filter.
`}\n
\n
`}function Mt({snap:t,delay:e,filter:n,setFilter:s}){let o=t.audit||[],a=t.auditMetrics||{byAction:{},bySource:{}},l=o.filter(i=>{if(n.action&&i.action!==n.action||n.source&&i.source!==n.source)return!1;let r=n.text.trim().toLowerCase();return!(r&&[i.key||\"\",i.action,i.source,i.detail||\"\",i.scope||\"\",i.env||\"\"].join(\" \").toLowerCase().indexOf(r)===-1)}),d=[\"read\",\"write\",\"delete\",\"rotate\",\"generate\",\"entangle\",\"tunnel\",\"teleport\",\"approve\",\"revoke\",\"policy_deny\",\"collapse\",\"export\"],_=[\"cli\",\"mcp\",\"agent\",\"ci\",\"hook\",\"api\"],c=(i,r)=>s({...n,[i]:n[i]===r?\"\":r});return u`<${M} delay=${e} wide=${!0}>\n <${S} icon=${m.audit} title=\"Audit Log (24h)\" aside=${l.length+\" of \"+o.length+\" shown \\xB7 window \"+(a.windowSeconds||86400)/3600+\"h\"} />\n
\n ${d.filter(i=>a.byAction[i]).length?d.filter(i=>a.byAction[i]).map(i=>u`c(\"action\",i)}>${i}${a.byAction[i]}`):u`no actions`}\n
\n ${_.filter(i=>a.bySource[i]).length?u`
${_.filter(i=>a.bySource[i]).map(i=>u`c(\"source\",i)}>${i}${a.bySource[i]}`)}
`:null}\n
\n \n \n
\n ${l.length?u`
${l.slice(0,80).map((i,r)=>u`
\n ${Xe(i.timestamp)}\n ${i.action}\n ${i.source}\n ${i.key||\"\\u2014\"} ${i.detail||\"\"}\n
`)}
`:u`<${E}>No audit events match the filter.`}\n `}function At(){let[t,e]=q(null),[n,s]=q(!1),[o,a]=q(\"connecting\"),[l,d]=q(\"\"),[_,c]=q({col:\"lastAccessedAt\",dir:\"desc\"}),[i,r]=q({action:\"\",source:\"\",text:\"\"}),[,p]=q(0),v=fe(n);v.current=n;let f=fe(null),C=ye($=>{try{e(typeof $==\"string\"?JSON.parse($):$)}catch{}},[]),k=ye(()=>{fetch(\"/api/status\"+he,{cache:\"no-store\"}).then($=>$.json()).then(C).catch(()=>{})},[C]);ee(()=>{let $=new EventSource(\"/events\"+he);return $.onopen=()=>a(\"live\"),$.onmessage=b=>{v.current||C(b.data)},$.onerror=()=>a(\"reconnecting\"),()=>$.close()},[C]),ee(()=>{let $=setInterval(()=>p(b=>b+1),5e3);return()=>clearInterval($)},[]),ee(()=>{let $=b=>{let T=b.target&&b.target.tagName;if(T===\"INPUT\"||T===\"TEXTAREA\"){b.key===\"Escape\"&&b.target.blur();return}b.key===\"/\"?(b.preventDefault(),f.current&&f.current.focus()):b.key===\"p\"||b.key===\"P\"?s(F=>!F):(b.key===\"r\"||b.key===\"R\")&&k()};return document.addEventListener(\"keydown\",$),()=>document.removeEventListener(\"keydown\",$)},[k]);let y=t?\"updated \"+Ze(t.timestamp):\"\\u2014\";if(!t)return u`
\n <${Ge} snap=${null} paused=${n} connState=${o} onPause=${()=>s($=>!$)} onRefresh=${k} lastUpdate=${y} />\n

Connecting to the live quantum stream…

\n
`;let h=t.auditMetrics||{total:0};return u`
\n <${Ge} snap=${t} paused=${n} connState=${o} onPause=${()=>s($=>!$)} onRefresh=${k} lastUpdate=${y} />\n <${ut} snap=${t} />\n
\n <${_t} snap=${t} delay=${0} />\n <${pt} snap=${t} delay=${60} />\n <${vt} snap=${t} delay=${120} />\n <${ft} snap=${t} delay=${180} />\n
\n <${St} snap=${t} query=${l} setQuery=${d} sort=${_} setSort=${c} searchRef=${f} />\n
\n <${yt} snap=${t} delay=${0} />\n <${ht} snap=${t} delay=${60} />\n <${$t} snap=${t} delay=${120} />\n <${mt} snap=${t} delay=${180} />\n
\n
\n <${gt} snap=${t} delay=${0} />\n <${kt} snap=${t} delay=${60} />\n <${bt} snap=${t} delay=${120} />\n
\n
\n <${xt} snap=${t} delay=${0} />\n <${Mt} snap=${t} delay=${60} filter=${i} setFilter=${r} />\n
\n

\n ${\"q-ring v\"+t.version+\" \\xB7 snapshot \"+Xe(t.timestamp)+\" \\xB7 \"+(h.total||0)+\" audit events in last 24h\"}\n · keyboard: / search · P pause · R refresh\n

\n
`}qe(u`<${At} />`,document.getElementById(\"app\"));})();"; +export const DASHBOARD_CLIENT = "\"use strict\";(()=>{var J,b,we,et,N,ge,Ce,Me,se,W,L,Se,le,ae,oe,tt,O={},Y=[],nt=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Q=Array.isArray;function R(t,e){for(var n in e)t[n]=e[n];return t}function re(t){t&&t.parentNode&&t.parentNode.removeChild(t)}function ce(t,e,n){var s,i,a,l={};for(a in e)a==\"key\"?s=e[a]:a==\"ref\"?i=e[a]:l[a]=e[a];if(arguments.length>2&&(l.children=arguments.length>3?J.call(arguments,2):n),typeof t==\"function\"&&t.defaultProps!=null)for(a in t.defaultProps)l[a]===void 0&&(l[a]=t.defaultProps[a]);return K(t,l,s,i,null)}function K(t,e,n,s,i){var a={type:t,props:e,key:n,ref:s,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:i==null?++we:i,__i:-1,__u:0};return i==null&&b.vnode!=null&&b.vnode(a),a}function X(t){return t.children}function z(t,e){this.props=t,this.context=e}function D(t,e){if(e==null)return t.__?D(t.__,t.__i+1):null;for(var n;ee&&N.sort(Me),t=N.shift(),e=N.length,st(t)}finally{N.length=G.__r=0}}function Ee(t,e,n,s,i,a,l,u,p,c,r){var o,_,v,m,h,y,f,$=s&&s.__k||Y,g=e.length;for(p=at(n,e,$,p,g),o=0;o0?l=t.__k[a]=K(l.type,l.props,l.key,l.ref?l.ref:null,l.__v):t.__k[a]=l,p=a+_,l.__=t,l.__b=t.__b+1,u=null,(c=l.__i=ot(l,n,p,o))!=-1&&(o--,(u=n[c])&&(u.__u|=2)),u==null||u.__v==null?(c==-1&&(i>r?_--:ip?_--:_++,l.__u|=4))):t.__k[a]=null;if(o)for(a=0;a(r?1:0)){for(i=n-1,a=n+1;i>=0||a=0?i--:a++])!=null&&(2&c.__u)==0&&u==c.key&&p==c.type)return l}return-1}function be(t,e,n){e[0]==\"-\"?t.setProperty(e,n==null?\"\":n):t[e]=n==null?\"\":typeof n!=\"number\"||nt.test(e)?n:n+\"px\"}function V(t,e,n,s,i){var a,l;e:if(e==\"style\")if(typeof n==\"string\")t.style.cssText=n;else{if(typeof s==\"string\"&&(t.style.cssText=s=\"\"),s)for(e in s)n&&e in n||be(t.style,e,\"\");if(n)for(e in n)s&&n[e]==s[e]||be(t.style,e,n[e])}else if(e[0]==\"o\"&&e[1]==\"n\")a=e!=(e=e.replace(Se,\"$1\")),l=e.toLowerCase(),e=l in t||e==\"onFocusOut\"||e==\"onFocusIn\"?l.slice(2):e.slice(2),t.l||(t.l={}),t.l[e+a]=n,n?s?n[L]=s[L]:(n[L]=le,t.addEventListener(e,a?oe:ae,a)):t.removeEventListener(e,a?oe:ae,a);else{if(i==\"http://www.w3.org/2000/svg\")e=e.replace(/xlink(H|:h)/,\"h\").replace(/sName$/,\"s\");else if(e!=\"width\"&&e!=\"height\"&&e!=\"href\"&&e!=\"list\"&&e!=\"form\"&&e!=\"tabIndex\"&&e!=\"download\"&&e!=\"rowSpan\"&&e!=\"colSpan\"&&e!=\"role\"&&e!=\"popover\"&&e in t)try{t[e]=n==null?\"\":n;break e}catch{}typeof n==\"function\"||(n==null||n===!1&&e[4]!=\"-\"?t.removeAttribute(e):t.setAttribute(e,e==\"popover\"&&n==1?\"\":n))}}function xe(t){return function(e){if(this.l){var n=this.l[e.type+t];if(e[W]==null)e[W]=le++;else if(e[W]0?t:Q(t)?t.map(Te):t.constructor!==void 0?null:R({},t)}function it(t,e,n,s,i,a,l,u,p){var c,r,o,_,v,m,h,y=n.props||O,f=e.props,$=e.type;if($==\"svg\"?i=\"http://www.w3.org/2000/svg\":$==\"math\"?i=\"http://www.w3.org/1998/Math/MathML\":i||(i=\"http://www.w3.org/1999/xhtml\"),a!=null){for(c=0;c=n.__.length&&n.__.push({}),n.__[t]}function q(t){return U=1,rt(Ke,t)}function rt(t,e,n){var s=ve(I++,2);if(s.t=t,!s.__c&&(s.__=[n?n(e):Ke(void 0,e),function(u){var p=s.__N?s.__N[0]:s.__[0],c=s.t(p,u);p!==c&&(s.__N=[c,s.__[1]],s.__c.setState({}))}],s.__c=w,!w.__f)){var i=function(u,p,c){if(!s.__c.__H)return!0;var r=s.__c.__H.__.filter(function(_){return _.__c});if(r.every(function(_){return!_.__N}))return!a||a.call(this,u,p,c);var o=s.__c.props!==u;return r.some(function(_){if(_.__N){var v=_.__[0];_.__=_.__N,_.__N=void 0,v!==_.__[0]&&(o=!0)}}),a&&a.call(this,u,p,c)||o};w.__f=!0;var a=w.shouldComponentUpdate,l=w.componentWillUpdate;w.componentWillUpdate=function(u,p,c){if(this.__e){var r=a;a=void 0,i(u,p,c),a=r}l&&l.call(this,u,p,c)},w.shouldComponentUpdate=i}return s.__N||s.__}function ee(t,e){var n=ve(I++,3);!C.__s&&We(n.__H,e)&&(n.__=t,n.u=e,w.__H.__h.push(n))}function fe(t){return U=5,te(function(){return{current:t}},[])}function te(t,e){var n=ve(I++,7);return We(n.__H,e)&&(n.__=t(),n.__H=e,n.__h=t),n.__}function he(t,e){return U=8,te(function(){return t},e)}function ct(){for(var t;t=Ve.shift();){var e=t.__H;if(t.__P&&e)try{e.__h.some(Z),e.__h.some(_e),e.__h=[]}catch(n){e.__h=[],C.__e(n,t.__v)}}}C.__b=function(t){w=null,De&&De(t)},C.__=function(t,e){t&&e.__k&&e.__k.__m&&(t.__m=e.__k.__m),Fe&&Fe(t,e)},C.__r=function(t){je&&je(t),I=0;var e=(w=t.__c).__H;e&&(pe===w?(e.__h=[],w.__h=[],e.__.some(function(n){n.__N&&(n.__=n.__N),n.u=n.__N=void 0})):(e.__h.some(Z),e.__h.some(_e),e.__h=[],I=0)),pe=w},C.diffed=function(t){Le&&Le(t);var e=t.__c;e&&e.__H&&(e.__H.__h.length&&(Ve.push(e)!==1&&He===C.requestAnimationFrame||((He=C.requestAnimationFrame)||dt)(ct)),e.__H.__.some(function(n){n.u&&(n.__H=n.u),n.u=void 0})),pe=w=null},C.__c=function(t,e){e.some(function(n){try{n.__h.some(Z),n.__h=n.__h.filter(function(s){return!s.__||_e(s)})}catch(s){e.some(function(i){i.__h&&(i.__h=[])}),e=[],C.__e(s,n.__v)}}),Ie&&Ie(t,e)},C.unmount=function(t){Ue&&Ue(t);var e,n=t.__c;n&&n.__H&&(n.__H.__.some(function(s){try{Z(s)}catch(i){e=i}}),n.__H=void 0,e&&C.__e(e,n.__v))};var Be=typeof requestAnimationFrame==\"function\";function dt(t){var e,n=function(){clearTimeout(s),Be&&cancelAnimationFrame(e),setTimeout(t)},s=setTimeout(n,35);Be&&(e=requestAnimationFrame(n))}function Z(t){var e=w,n=t.__c;typeof n==\"function\"&&(t.__c=void 0,n()),w=e}function _e(t){var e=w;t.__c=t.__(),w=e}function We(t,e){return!t||t.length!==e.length||e.some(function(n,s){return n!==t[s]})}function Ke(t,e){return typeof e==\"function\"?e(t):e}var Oe=function(t,e,n,s){var i;e[0]=0;for(var a=1;a=5&&((l||!_&&a===5)&&(p.push(a,0,l,i),a=6),_&&(p.push(a,_,0,i),a=6)),l=\"\"},r=0;r\"?(a=1,l=\"\"):l=s+l[0]:u?s===u?u=\"\":l+=s:s==='\"'||s===\"'\"?u=s:s===\">\"?(c(),a=1):a&&(s===\"=\"?(a=5,i=l,l=\"\"):s===\"/\"&&(a<5||n[r][o+1]===\">\")?(c(),a===3&&(p=p[0]),a=p,(p=p[0]).push(2,0,a),a=0):s===\" \"||s===\"\t\"||s===`\n`||s===\"\\r\"?(c(),a=2):l+=s),a===3&&l===\"!--\"&&(a=4,p=p[0])}return c(),p})(t)),e),arguments,[])).length>1?e:e[0]}var d=Ye.bind(ce),Ge=window.__QRING__&&window.__QRING__.token||\"\",ye=Ge?\"?token=\"+encodeURIComponent(Ge):\"\",A=t=>''+t+\"\",k={health:A(''),environment:A(''),decay:A(''),superposition:A(''),entangle:A(''),tunnel:A(''),anomaly:A(''),audit:A(''),key:A(''),hook:A(''),approve:A(''),manifest:A(''),policy:A(''),memory:A(''),folder:'',search:'',lock:''};function me(t){if(!t)return\"env-default\";let e=t.toLowerCase();return e===\"prod\"||e===\"production\"?\"env-prod\":e===\"staging\"||e===\"stage\"?\"env-staging\":e===\"dev\"||e===\"development\"?\"env-dev\":e===\"test\"||e===\"testing\"?\"env-test\":\"env-default\"}function Qe(t,e){return e||t>=90?\"var(--danger)\":t>=75?\"var(--warning)\":\"var(--accent)\"}function Xe(t){let e=new Date(t);return String(e.getHours()).padStart(2,\"0\")+\":\"+String(e.getMinutes()).padStart(2,\"0\")+\":\"+String(e.getSeconds()).padStart(2,\"0\")}function Ze(t){if(!t)return\"\\u2014\";let e=Math.floor((Date.now()-new Date(t).getTime())/1e3);return isNaN(e)?\"\\u2014\":e<5?\"just now\":e<60?e+\"s ago\":e<3600?Math.floor(e/60)+\"m ago\":e<86400?Math.floor(e/3600)+\"h ago\":Math.floor(e/86400)+\"d ago\"}function $e(t){return t==null?\"\\u2014\":t<0?\"expired\":t<60?t+\"s\":t<3600?Math.floor(t/60)+\"m\":t<86400?Math.floor(t/3600)+\"h\":Math.floor(t/86400)+\"d\"}var j=({svg:t})=>d``;function M({icon:t,title:e,aside:n,asideStyle:s}){return d`
\n <${j} svg=${t} />${e}\n ${n!=null?d`${n}`:null}\n
`}function S({delay:t=0,wide:e=!1,style:n=\"\",children:s}){let i=\"card\"+(e?\" grid-wide\":\"\"),a=\"animation-delay:\"+t+\"ms\"+(n?\";\"+n:\"\");return d`
${s}
`}function E({children:t,cta:e}){return d`
${t}${e?d`${e}`:null}
`}function Je({snap:t,paused:e,connState:n,onPause:s,onRefresh:i,lastUpdate:a}){let l=e?\"paused\":n===\"live\"?\"live\":n===\"reconnecting\"?\"reconnecting\\u2026\":\"connecting\\u2026\",u=\"status-dot\"+(e?\" paused\":n===\"reconnecting\"?\" disconnected\":\"\");return d`\n
\n
\n

\n \n q-ring\n quantum status\n

\n \n \n ${\" v\"+(t?t.version:\"\")}\n \n \n <${j} svg=${k.folder} /> ${t?t.projectPath:\"\"}\n \n
\n
\n ${a}\n \n \n \n \n JSON\n \n ${l}\n
\n
\n `}function ut({snap:t}){let e=t,n=e.auditMetrics||{total:0,byAction:{},topRead:[]},s=n.byAction.read||0,i=(n.byAction.write||0)+(n.byAction.delete||0)+(n.byAction.rotate||0),a=n.byAction.policy_deny||0,l=(e.hooks||[]).filter(r=>r.enabled).length,u=(e.approvals||[]).filter(r=>r.valid).length,p=(e.anomalies||[]).length,c=[{label:\"Secrets\",icon:k.key,value:e.health.total,sub:e.health.healthy+\" healthy \\xB7 \"+e.health.stale+\" stale \\xB7 \"+e.health.expired+\" expired\",cls:\"\"},{label:\"Environment\",icon:k.environment,value:e.environment?e.environment.env:\"none\",sub:e.environment?\"via \"+e.environment.source:\"no env detected\",cls:e.environment?\"\":\"dim\"},{label:\"Protected\",icon:k.lock,value:e.protectedCount,sub:\"require approval\",cls:e.protectedCount>0?\"warning\":\"dim\"},{label:\"Approvals\",icon:k.approve,value:u,sub:\"active grants\",cls:u>0?\"green\":\"dim\"},{label:\"Hooks\",icon:k.hook,value:l,sub:\"enabled / \"+(e.hooks||[]).length+\" total\",cls:l>0?\"\":\"dim\"},{label:\"Reads (24h)\",icon:k.audit,value:s,sub:i+\" writes \\xB7 \"+a+\" denied\",cls:\"\"},{label:\"Anomalies\",icon:k.anomaly,value:p,sub:p?\"investigate now\":\"all clear\",cls:p?\"danger\":\"green\"}];return d`
\n ${c.map(r=>d`
\n
<${j} svg=${r.icon} />${r.label}
\n
${r.value}
\n
${r.sub}
\n
`)}\n
`}function pt({snap:t,delay:e}){let n=t.health,s=n.total||1,i=42,a=2*Math.PI*i,l=[{v:n.healthy,c:\"var(--accent)\"},{v:n.stale,c:\"var(--warning)\"},{v:n.expired,c:\"var(--danger)\"},{v:n.noDecay,c:\"var(--text-dim)\"}],u=0,p=l.map((r,o)=>{let _=r.v/s*a,v=d``;return u+=_,v}),c=t.scopes||{global:0,project:0,team:0,org:0};return d`<${S} delay=${e}>\n <${M} icon=${k.health} title=\"Health Summary\" aside=${n.healthy+\"/\"+n.total+\" healthy\"} />\n
\n
\n ${p}\n
${n.total}secrets
\n
\n
\n
Healthy ${n.healthy}
\n
Stale ${n.stale}
\n
Expired ${n.expired}
\n
No decay ${n.noDecay}
\n
\n
\n
\n ${[\"global\",\"project\",\"team\",\"org\"].filter(r=>c[r]).map(r=>d`${c[r]}${r}`)}\n
\n `}function _t({snap:t,delay:e}){let n=t.environment;return d`<${S} delay=${e}>\n <${M} icon=${k.environment} title=\"Environment\" />\n ${n?d`
${n.env}detected via ${n.source}
`:d`<${E} cta=\"$ qring env --project-path .\">No environment detected.`}\n `}function vt({snap:t,delay:e}){let n=t.manifest;if(!n)return d`<${S} delay=${e}>\n <${M} icon=${k.manifest} title=\"Manifest\" />\n <${E} cta=${d`$ qring wizard `}>No .q-ring.json manifest in this project.\n `;let s=n.required-n.missing.length-n.expired.length-n.stale.length,i=n.required?Math.max(0,Math.round(s/n.required*100)):100,a=n.missing.length?\"var(--danger)\":n.expired.length||n.stale.length?\"var(--warning)\":\"var(--green)\",l=(p,c,r)=>d`
${p} (${c.length})
${c.map(o=>d`${o}`)}
`,u=n.missing.length||n.expired.length||n.stale.length;return d`<${S} delay=${e}>\n <${M} icon=${k.manifest} title=\"Manifest\" aside=${n.required+\" required / \"+n.declared+\" declared\"} />\n
\n
healthy
${i}%
\n
\n
\n ${n.missing.length?l(\"Missing required\",n.missing,\"miss\"):null}\n ${n.expired.length?l(\"Expired\",n.expired,\"exp\"):null}\n ${n.stale.length?l(\"Stale\",n.stale,\"exp\"):null}\n ${u?null:d`
All required keys present and healthy.
`}\n
\n `}function ft({snap:t,delay:e}){let n=t.policy||{counts:{}},s=n.counts||{};if(!(n.hasMcpPolicy||n.hasExecPolicy||n.hasSecretPolicy))return d`<${S} delay=${e}>\n <${M} icon=${k.policy} title=\"Policy\" />\n <${E} cta=${d`add policy to .q-ring.json`}>No governance policy declared.\n `;let a=({label:u,value:p})=>d`
${u}${p||\"\\u2014\"}
`,l=[n.hasMcpPolicy?\"mcp\":\"\",n.hasExecPolicy?\"exec\":\"\",n.hasSecretPolicy?\"secrets\":\"\"].filter(Boolean).join(\" \\xB7 \")||\"none\";return d`<${S} delay=${e}>\n <${M} icon=${k.policy} title=\"Policy\" aside=${l} />\n
\n <${a} label=\"MCP allow tools\" value=${s.allowTools} />\n <${a} label=\"MCP deny tools\" value=${s.denyTools} />\n <${a} label=\"Denied keys\" value=${s.deniedKeys} />\n <${a} label=\"Denied tags\" value=${s.deniedTags} />\n <${a} label=\"Exec allow\" value=${s.allowCommands} />\n <${a} label=\"Exec deny\" value=${s.denyCommands} />\n <${a} label=\"Tag → approval\" value=${s.requireApprovalForTags} />\n <${a} label=\"Tag → rotation\" value=${s.requireRotationFormatForTags} />\n ${n.maxTtlSeconds?d`<${a} label=\"Max TTL\" value=${$e(n.maxTtlSeconds)} />`:null}\n ${n.maxRuntimeSeconds?d`<${a} label=\"Max exec runtime\" value=${$e(n.maxRuntimeSeconds)} />`:null}\n
\n `}function ht({snap:t,delay:e}){let n=(t.secrets||[]).filter(s=>s.decay&&s.decay.timeRemaining).sort((s,i)=>(s.decay.secondsRemaining||0)-(i.decay.secondsRemaining||0));return d`<${S} delay=${e}>\n <${M} icon=${k.decay} title=\"Decay Timers\" aside=${n.length+\" tracked\"} />\n ${n.length?d`
${n.slice(0,12).map(s=>{let i=Math.min(s.decay.lifetimePercent,100),a=Qe(i,s.decay.isExpired),l=s.decay.isExpired?\"expired\":s.decay.timeRemaining||\"\";return d`
\n ${s.key}\n
\n ${l}\n
`})}
`:d`<${E} cta=\"$ qring set KEY --ttl 86400\">No secrets with decay configured.`}\n `}function yt({snap:t,delay:e}){let n=(t.secrets||[]).filter(s=>s.type===\"superposition\"&&s.environments&&s.environments.length);return d`<${S} delay=${e}>\n <${M} icon=${k.superposition} title=\"Superposition\" aside=${n.length+\" multi-env\"} />\n ${n.length?d`
${n.slice(0,14).map(s=>d`
\n ${s.key}\n ${(s.environments||[]).map(i=>d`${i}${i===s.defaultEnv?\" \\u2713\":\"\"}`)}\n
`)}
`:d`<${E} cta=\"$ qring set KEY --env prod\">No secrets in superposition.`}\n `}function $t({snap:t,delay:e}){let n=t.entanglements||[],s=new Set,i=n.filter(h=>{let y=[h.source.service,h.source.key,h.target.service,h.target.key].sort().join(\"|\");return s.has(y)?!1:(s.add(y),!0)}),a=[],l=new Map;for(let h of i)for(let y of[h.source.key,h.target.key])l.has(y)||(l.set(y,a.length),a.push(y));let u=i.length>0&&a.length<=14,p=320,c=210,r=p/2,o=c/2,_=Math.min(p,c)/2-34,v=a.map((h,y)=>{let f=2*Math.PI*y/a.length-Math.PI/2;return{x:r+_*Math.cos(f),y:o+_*Math.sin(f)}}),m=h=>h.length>14?h.slice(0,12)+\"\\u2026\":h;return d`<${S} delay=${e}>\n <${M} icon=${k.entangle} title=\"Entanglement\" aside=${i.length+\" pair\"+(i.length===1?\"\":\"s\")+\" \\xB7 \"+a.length+\" keys\"} />\n ${i.length?u?d`\n ${i.map((h,y)=>{let f=v[l.get(h.source.key)],$=v[l.get(h.target.key)];return d``})}\n ${a.map((h,y)=>{let f=v[y],$=f.x>=r;return d`\n \n ${m(h)}${h}\n `})}\n `:d`
${i.slice(0,12).map((h,y)=>d`
\n ${h.source.key}${h.target.key}\n
`)}
`:d`<${E} cta=\"$ qring entangle KEY_A KEY_B\">No entangled secrets.`}\n `}function mt({snap:t,delay:e}){let n=t.tunnels||[];return d`<${S} delay=${e}>\n <${M} icon=${k.tunnel} title=\"Quantum Tunnels\" aside=${n.length+\" live\"} />\n ${n.length?d`
${n.map(s=>{let i=s.expiresAt?Math.max(0,Math.floor((s.expiresAt-Date.now())/1e3)):null;return d`
${s.id}
\n reads: ${s.accessCount}${s.maxReads?\"/\"+s.maxReads:\"\"}\n ${i!==null?d`expires: ${i}s`:d`no expiry`}\n
`})}
`:d`<${E} cta=${d`$ qring tunnel create `}>No active tunnels.`}\n `}function gt({snap:t,delay:e}){let n=t.approvals||[];return d`<${S} delay=${e}>\n <${M} icon=${k.approve} title=\"Approvals\" aside=${n.filter(s=>s.valid).length+\" valid\"} />\n ${n.length?d`
${n.slice(0,8).map(s=>{let i=s.tampered?\"tampered\":s.secondsRemaining<300?\"expiring\":\"\";return d`
\n
${s.key}${s.tampered?\" \\u26A0 TAMPERED\":\"\"}${$e(s.secondsRemaining)} left
\n
${s.reason||\"(no reason)\"}
\n
scope:${s.scope}by:${s.grantedBy}
\n
`})}
`:d`<${E} cta=${'$ qring approve KEY --for 1800 --reason \"...\"'}>No active approvals.`}\n `}function kt({snap:t,delay:e}){let n=t.hooks||[];return d`<${S} delay=${e}>\n <${M} icon=${k.hook} title=\"Hooks\" aside=${n.filter(s=>s.enabled).length+\" enabled / \"+n.length} />\n ${n.length?d`
${n.slice(0,8).map(s=>d`
\n ${s.type}\n ${s.description||s.matchSummary}\n ${s.id}\n
`)}
`:d`<${E} cta=${'$ qring hook add --tag payments --action rotate --exec \"...\"'}>No hooks registered.`}\n `}function bt({snap:t,delay:e}){return d`<${S} delay=${e}>\n <${M} icon=${k.memory} title=\"Agent Memory\" />\n
\n ${t.memoryKeys||0}\n memory key${t.memoryKeys===1?\"\":\"s\"} encrypted at ~/.config/q-ring/agent-memory.enc\n
\n `}function xt({snap:t,delay:e}){let n=t.anomalies||[];if(!n.length)return d`<${S} delay=${e} wide=${!0}>\n <${M} icon=${k.anomaly} title=\"Anomaly Alerts\" aside=\"all clear\" asideStyle=\"color:var(--green)\" />\n <${E}>No anomalies detected — quantum field is stable.\n `;let s=i=>i===\"tampered\"?\"$ qring audit:verify\":i===\"burst\"?\"$ qring audit --key \\u2026 --action read\":i===\"unusual-hour\"?'$ qring audit --action read --since \"1am\"':\"$ qring audit --limit 100\";return d`<${S} delay=${e} wide=${!0}>\n <${M} icon=${k.anomaly} title=\"Anomaly Alerts\" aside=${n.length+\" active\"} asideStyle=\"color:var(--danger)\" />\n
${n.map((i,a)=>d`
\n
${i.type}
\n
${i.description}
\n
${s(i.type)}
\n
`)}
\n `}function wt(t,e){let n=e.trim().toLowerCase();return n?n===\"expired\"?t.filter(s=>s.decay&&s.decay.isExpired):n===\"stale\"?t.filter(s=>s.decay&&s.decay.isStale&&!s.decay.isExpired):n===\"protected\"?t.filter(s=>s.requiresApproval):t.filter(s=>[s.key,s.scope,s.type,s.provider||\"\",s.defaultEnv||\"\",(s.environments||[]).join(\" \"),(s.tags||[]).join(\" \")].join(\" \").toLowerCase().indexOf(n)!==-1):t.slice()}function Ct(t,e){let n=e.dir===\"asc\"?1:-1;return t.sort((s,i)=>{let a,l;switch(e.col){case\"key\":a=s.key,l=i.key;break;case\"scope\":a=s.scope,l=i.scope;break;case\"env\":a=s.defaultEnv||(s.environments||[])[0]||\"\",l=i.defaultEnv||(i.environments||[])[0]||\"\";break;case\"type\":a=s.type,l=i.type;break;case\"decay\":a=s.decay&&s.decay.secondsRemaining!=null?s.decay.secondsRemaining:1/0,l=i.decay&&i.decay.secondsRemaining!=null?i.decay.secondsRemaining:1/0;break;case\"tags\":a=(s.tags||[]).join(\",\"),l=(i.tags||[]).join(\",\");break;default:a=s.lastAccessedAt?new Date(s.lastAccessedAt).getTime():0,l=i.lastAccessedAt?new Date(i.lastAccessedAt).getTime():0}return al?1*n:0})}function Mt({snap:t,query:e,setQuery:n,sort:s,setSort:i,searchRef:a}){let l=t.secrets||[],u=te(()=>Ct(wt(l,e),s),[l,e,s]),p=[{id:\"key\",label:\"Key\"},{id:\"scope\",label:\"Scope\"},{id:\"env\",label:\"Env\"},{id:\"type\",label:\"Type\"},{id:\"decay\",label:\"Decay\"},{id:\"tags\",label:\"Tags\"},{id:\"lastAccessedAt\",label:\"Last read\"}],c=r=>{s.col===r?i({col:r,dir:s.dir===\"asc\"?\"desc\":\"asc\"}):i({col:r,dir:r===\"key\"?\"asc\":\"desc\"})};return d`
<${S} wide=${!0} style=\"margin-bottom:16px\">\n <${M} icon=${k.key} title=\"Secrets\" aside=${u.length+\" of \"+l.length} />\n
\n \n \n \n \n \n
\n
\n ${u.length?d`\n ${p.map(r=>d``)}\n ${u.map(r=>{let o=r.type===\"superposition\"?(r.environments||[]).map(m=>d`${m}${m===r.defaultEnv?\" \\u2713\":\"\"}`):d``,_=r.decay&&r.decay.timeRemaining?(()=>{let m=Math.min(r.decay.lifetimePercent,100),h=Qe(m,r.decay.isExpired);return d`${r.decay.isExpired?\"expired\":r.decay.timeRemaining}`})():d``,v=r.tags||[];return d`\n \n \n \n \n \n \n \n `})}\n
c(r.id)}>${r.label}${s.dir===\"asc\"?\"\\u25B2\":\"\\u25BC\"}
${r.key}${r.requiresApproval?d`<${j} svg=${k.lock} />`:null}${r.scope}${o}${r.type}${_}${v.slice(0,3).map(m=>d`${m}`)}${v.length>3?\"+\"+(v.length-3):\"\"}${Ze(r.lastAccessedAt)}
`:d`
No secrets match the filter.
`}\n
\n
`}function St({snap:t,delay:e,filter:n,setFilter:s}){let i=t.audit||[],a=t.auditMetrics||{byAction:{},bySource:{}},l=i.filter(o=>{if(n.action&&o.action!==n.action||n.source&&o.source!==n.source)return!1;let _=n.text.trim().toLowerCase();return!(_&&[o.key||\"\",o.action,o.source,o.detail||\"\",o.scope||\"\",o.env||\"\"].join(\" \").toLowerCase().indexOf(_)===-1)}),u=[\"read\",\"write\",\"delete\",\"rotate\",\"generate\",\"entangle\",\"tunnel\",\"teleport\",\"approve\",\"revoke\",\"policy_deny\",\"collapse\",\"export\"],p=[\"cli\",\"mcp\",\"agent\",\"ci\",\"hook\",\"api\"],c=(o,_)=>s({...n,[o]:n[o]===_?\"\":_}),r=t.auditChain;return d`<${S} delay=${e} wide=${!0}>\n <${M} icon=${k.audit} title=\"Audit Log (24h)\" aside=${l.length+\" of \"+i.length+\" shown \\xB7 window \"+(a.windowSeconds||86400)/3600+\"h\"} />\n ${r&&r.totalEvents>0?d`
\n ${r.intact?d`⛓ chain intact · ${r.totalEvents} events verified`:d`⛓ chain BROKEN at event #${r.brokenAt} · ${r.validEvents}/${r.totalEvents} valid — run $ qring audit:verify`}\n
`:null}\n
\n ${u.filter(o=>a.byAction[o]).length?u.filter(o=>a.byAction[o]).map(o=>d`c(\"action\",o)}>${o}${a.byAction[o]}`):d`no actions`}\n
\n ${p.filter(o=>a.bySource[o]).length?d`
${p.filter(o=>a.bySource[o]).map(o=>d`c(\"source\",o)}>${o}${a.bySource[o]}`)}
`:null}\n
\n \n \n
\n ${l.length?d`
${l.slice(0,80).map((o,_)=>d`
\n ${Xe(o.timestamp)}\n ${o.action}\n ${o.source}\n ${o.key||\"\\u2014\"} ${o.detail||\"\"}\n
`)}
`:d`<${E}>No audit events match the filter.`}\n `}function At(){let[t,e]=q(null),[n,s]=q(!1),[i,a]=q(\"connecting\"),[l,u]=q(\"\"),[p,c]=q({col:\"lastAccessedAt\",dir:\"desc\"}),[r,o]=q({action:\"\",source:\"\",text:\"\"}),[,_]=q(0),v=fe(n);v.current=n;let m=fe(null),h=he(g=>{try{e(typeof g==\"string\"?JSON.parse(g):g)}catch{}},[]),y=he(()=>{fetch(\"/api/status\"+ye,{cache:\"no-store\"}).then(g=>g.json()).then(h).catch(()=>{})},[h]);ee(()=>{let g=new EventSource(\"/events\"+ye);return g.onopen=()=>a(\"live\"),g.onmessage=x=>{v.current||h(x.data)},g.onerror=()=>a(\"reconnecting\"),()=>g.close()},[h]),ee(()=>{let g=setInterval(()=>_(x=>x+1),5e3);return()=>clearInterval(g)},[]),ee(()=>{let g=x=>{let T=x.target&&x.target.tagName;if(T===\"INPUT\"||T===\"TEXTAREA\"){x.key===\"Escape\"&&x.target.blur();return}x.key===\"/\"?(x.preventDefault(),m.current&&m.current.focus()):x.key===\"p\"||x.key===\"P\"?s(F=>!F):(x.key===\"r\"||x.key===\"R\")&&y()};return document.addEventListener(\"keydown\",g),()=>document.removeEventListener(\"keydown\",g)},[y]);let f=t?\"updated \"+Ze(t.timestamp):\"\\u2014\";if(!t)return d`
\n <${Je} snap=${null} paused=${n} connState=${i} onPause=${()=>s(g=>!g)} onRefresh=${y} lastUpdate=${f} />\n

Connecting to the live quantum stream…

\n
`;let $=t.auditMetrics||{total:0};return d`
\n <${Je} snap=${t} paused=${n} connState=${i} onPause=${()=>s(g=>!g)} onRefresh=${y} lastUpdate=${f} />\n <${ut} snap=${t} />\n
\n <${pt} snap=${t} delay=${0} />\n <${_t} snap=${t} delay=${60} />\n <${vt} snap=${t} delay=${120} />\n <${ft} snap=${t} delay=${180} />\n
\n <${Mt} snap=${t} query=${l} setQuery=${u} sort=${p} setSort=${c} searchRef=${m} />\n
\n <${ht} snap=${t} delay=${0} />\n <${yt} snap=${t} delay=${60} />\n <${$t} snap=${t} delay=${120} />\n <${mt} snap=${t} delay=${180} />\n
\n
\n <${gt} snap=${t} delay=${0} />\n <${kt} snap=${t} delay=${60} />\n <${bt} snap=${t} delay=${120} />\n
\n
\n <${xt} snap=${t} delay=${0} />\n <${St} snap=${t} delay=${60} filter=${r} setFilter=${o} />\n
\n

\n ${\"q-ring v\"+t.version+\" \\xB7 snapshot \"+Xe(t.timestamp)+\" \\xB7 \"+($.total||0)+\" audit events in last 24h\"}\n · keyboard: / search · P pause · R refresh\n

\n
`}qe(d`<${At} />`,document.getElementById(\"app\"));})();"; diff --git a/src/core/dashboard-html.ts b/src/core/dashboard-html.ts index fbc1ec2..3b6230d 100644 --- a/src/core/dashboard-html.ts +++ b/src/core/dashboard-html.ts @@ -173,6 +173,15 @@ body{min-height:100vh;overflow-x:hidden;position:relative} .entangle-list{display:flex;flex-direction:column;gap:6px;max-height:240px;overflow-y:auto} .entangle-pair{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.83rem} .entangle-arrow{color:var(--accent-bright)} +.entangle-graph{display:block;width:100%;height:auto;max-height:260px} +.entangle-edge{stroke:var(--accent);stroke-opacity:.45;stroke-width:1.2} +.entangle-node{fill:var(--accent-bright);stroke:var(--accent-glow);stroke-width:3;paint-order:stroke} +.entangle-label{fill:var(--text-secondary);font-family:var(--font-mono);font-size:9px} + +/* Audit chain badge */ +.chain-badge{display:inline-flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:.78rem;padding:4px 10px;border-radius:999px;margin-bottom:10px} +.chain-badge.ok{color:var(--green);background:rgba(34,197,94,.1);border:1px solid rgba(34,197,94,.25)} +.chain-badge.broken{color:var(--danger);background:rgba(255,94,91,.12);border:1px solid rgba(255,94,91,.3)} /* Tunnels */ .tunnel-list{display:flex;flex-direction:column;gap:8px;max-height:240px;overflow-y:auto} diff --git a/src/core/dashboard.ts b/src/core/dashboard.ts index d63249b..1b1d17f 100644 --- a/src/core/dashboard.ts +++ b/src/core/dashboard.ts @@ -11,7 +11,7 @@ import { listSecrets } from "./keyring.js"; import { checkDecay, type DecayStatus, type QuantumEnvelope } from "./envelope.js"; import { listEntanglements, type EntanglementPair } from "./entanglement.js"; import { tunnelList } from "./tunnel.js"; -import { queryAudit, detectAnomalies, type AuditEvent, type AccessAnomaly, type AuditAction } from "./observer.js"; +import { queryAudit, detectAnomalies, verifyAuditChain, type AuditEvent, type AccessAnomaly, type AuditAction } from "./observer.js"; import { collapseEnvironment, readProjectConfig, type CollapseResult } from "./collapse.js"; import { listHooks, type HookEntry, type HookType } from "./hooks.js"; import { listApprovals } from "./approval.js"; @@ -167,6 +167,13 @@ export interface DashboardSnapshot { hooks: HookSnapshot[]; /** Agent memory key count */ memoryKeys: number; + /** Audit hash-chain integrity (same data as `qring audit:verify`) */ + auditChain: { + intact: boolean; + totalEvents: number; + validEvents: number; + brokenAt?: number; + }; } const AUDIT_WINDOW_SECONDS = 24 * 60 * 60; // 24h @@ -379,6 +386,15 @@ export function collectSnapshot(): DashboardSnapshot { approvals, hooks: hookEntries, memoryKeys: listMemory().length, + auditChain: (() => { + const chain = verifyAuditChain(); + return { + intact: chain.intact, + totalEvents: chain.totalEvents, + validEvents: chain.validEvents, + ...(chain.brokenAt != null ? { brokenAt: chain.brokenAt } : {}), + }; + })(), }; } @@ -386,6 +402,20 @@ export interface DashboardServerOptions { port?: number; } +// Enforced on every response. The page is fully self-contained (inline +// CSS/JS, no external fetches), so the CSP can deny everything except inline +// code and same-origin SSE. `no-referrer` keeps the ?token= URL out of any +// outbound Referer header; `no-store` keeps the token-bearing HTML out of +// shared caches. +const SECURITY_HEADERS: Record = { + "Content-Security-Policy": + "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'", + "X-Frame-Options": "DENY", + "X-Content-Type-Options": "nosniff", + "Referrer-Policy": "no-referrer", + "Cache-Control": "no-store", +}; + function timingSafeStringEqual(a: string, b: string): boolean { const ab = Buffer.from(a, "utf8"); const bb = Buffer.from(b, "utf8"); @@ -435,20 +465,21 @@ export function startDashboardServer( pathname = parsed.pathname; query = parsed.searchParams; } catch { - res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" }); + res.writeHead(400, { ...SECURITY_HEADERS, "Content-Type": "text/plain; charset=utf-8" }); res.end("Bad Request: invalid URL"); return; } const provided = query.get("token") ?? ""; if (!timingSafeStringEqual(provided, token)) { - res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" }); + res.writeHead(403, { ...SECURITY_HEADERS, "Content-Type": "text/plain; charset=utf-8" }); res.end("Forbidden: missing or invalid token"); return; } if (pathname === "/events") { res.writeHead(200, { + ...SECURITY_HEADERS, "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", @@ -465,13 +496,14 @@ export function startDashboardServer( if (pathname === "/api/status") { const snapshot = collectSnapshot(); res.writeHead(200, { + ...SECURITY_HEADERS, "Content-Type": "application/json", }); res.end(JSON.stringify(snapshot, null, 2)); return; } - res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); + res.writeHead(200, { ...SECURITY_HEADERS, "Content-Type": "text/html; charset=utf-8" }); res.end(html); }); diff --git a/src/dashboard-client/main.js b/src/dashboard-client/main.js index d2c74af..7b9fdcc 100644 --- a/src/dashboard-client/main.js +++ b/src/dashboard-client/main.js @@ -344,13 +344,52 @@ function EntanglementCard({ snap, delay }) { seen.add(id); return true; }); + + // Node/edge graph on a circular layout. Nodes are unique keys; an edge per + // entangled pair. Falls back to the flat list when the graph gets crowded. + const nodeNames = []; + const nodeIndex = new Map(); + for (const p of unique) { + for (const k of [p.source.key, p.target.key]) { + if (!nodeIndex.has(k)) { + nodeIndex.set(k, nodeNames.length); + nodeNames.push(k); + } + } + } + const useGraph = unique.length > 0 && nodeNames.length <= 14; + + const W = 320, H = 210, cx = W / 2, cy = H / 2; + const r = Math.min(W, H) / 2 - 34; + const pos = nodeNames.map((_, i) => { + const a = (2 * Math.PI * i) / nodeNames.length - Math.PI / 2; + return { x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) }; + }); + const trunc = (s) => (s.length > 14 ? s.slice(0, 12) + "…" : s); + return html`<${Card} delay=${delay}> - <${CardTitle} icon=${icons.entangle} title="Entanglement" aside=${unique.length + " pair" + (unique.length === 1 ? "" : "s")} /> + <${CardTitle} icon=${icons.entangle} title="Entanglement" aside=${unique.length + " pair" + (unique.length === 1 ? "" : "s") + " · " + nodeNames.length + " keys"} /> ${!unique.length ? html`<${Empty} cta="$ qring entangle KEY_A KEY_B">No entangled secrets.` - : html`
${unique.slice(0, 12).map((p, i) => html`
- ${p.source.key}${p.target.key} -
`)}
`} + : useGraph + ? html` + ${unique.map((p, i) => { + const a = pos[nodeIndex.get(p.source.key)]; + const b = pos[nodeIndex.get(p.target.key)]; + return html``; + })} + ${nodeNames.map((name, i) => { + const p = pos[i]; + const rightHalf = p.x >= cx; + return html` + + ${trunc(name)}${name} + `; + })} + ` + : html`
${unique.slice(0, 12).map((p, i) => html`
+ ${p.source.key}${p.target.key} +
`)}
`} `; } @@ -542,8 +581,16 @@ function AuditCard({ snap, delay, filter, setFilter }) { const allActions = ["read", "write", "delete", "rotate", "generate", "entangle", "tunnel", "teleport", "approve", "revoke", "policy_deny", "collapse", "export"]; const allSources = ["cli", "mcp", "agent", "ci", "hook", "api"]; const toggle = (field, val) => setFilter({ ...filter, [field]: filter[field] === val ? "" : val }); + const chain = snap.auditChain; return html`<${Card} delay=${delay} wide=${true}> <${CardTitle} icon=${icons.audit} title="Audit Log (24h)" aside=${filtered.length + " of " + events.length + " shown · window " + (m.windowSeconds || 86400) / 3600 + "h"} /> + ${chain && chain.totalEvents > 0 + ? html`
+ ${chain.intact + ? html`⛓ chain intact · ${chain.totalEvents} events verified` + : html`⛓ chain BROKEN at event #${chain.brokenAt} · ${chain.validEvents}/${chain.totalEvents} valid — run $ qring audit:verify`} +
` + : null}
${allActions.filter((a) => m.byAction[a]).length ? allActions.filter((a) => m.byAction[a]).map((a) => html` toggle("action", a)}>${a}${m.byAction[a]}`) diff --git a/src/index.ts b/src/index.ts index 6c610d4..8aaaa64 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,18 @@ import { createProgram } from "./cli/commands.js"; +import { c, SYMBOLS } from "./utils/colors.js"; const program = createProgram(); -program.parse(); + +// Central error handler: policy denials, keyring backend failures, bad input +// from core throw plain Errors. Show the message, not a Node stack trace. +// Set QRING_DEBUG=1 to get the full stack for bug reports. +try { + await program.parseAsync(); +} catch (err) { + const message = err instanceof Error ? err.message : String(err); + console.error(c.red(`${SYMBOLS.cross} ${message}`)); + if (process.env.QRING_DEBUG && err instanceof Error && err.stack) { + console.error(c.dim(err.stack)); + } + process.exitCode = 1; +} diff --git a/src/utils/prompt.ts b/src/utils/prompt.ts index 5a03a0b..8c120f3 100644 --- a/src/utils/prompt.ts +++ b/src/utils/prompt.ts @@ -1,6 +1,55 @@ import { createInterface } from "node:readline"; +/** + * Ask the user to confirm a destructive action. Resolves true only on an + * explicit yes. Non-interactive callers must pass `assumeYes` (--yes/-y); + * otherwise a non-TTY stdin is an error rather than a silent yes. + */ +export async function confirm( + message: string, + { assumeYes = false }: { assumeYes?: boolean } = {}, +): Promise { + if (assumeYes) return true; + if (!process.stdin.isTTY) { + throw new Error( + "Confirmation required but stdin is not a terminal. Pass --yes to proceed non-interactively.", + ); + } + + const rl = createInterface({ + input: process.stdin, + output: process.stderr, + terminal: true, + }); + + const answer = await new Promise((resolve) => { + rl.question(`${message} [y/N] `, resolve); + }); + rl.close(); + return /^y(es)?$/i.test(answer.trim()); +} + export async function promptSecret(message: string): Promise { + const stdin = process.stdin; + + // Non-interactive stdin (e.g. `echo "$VALUE" | qring set KEY`): read to EOF. + // The interactive path below waits on per-keypress data events that never + // arrive from a pipe, which would hang forever. + if (!stdin.isTTY) { + let data = ""; + stdin.setEncoding("utf8"); + for await (const chunk of stdin) { + data += chunk; + } + const value = data.replace(/\r?\n$/, ""); + if (value.length === 0) { + throw new Error( + "No value provided: stdin is not a terminal and was empty. Pipe the value in, or pass it as an argument.", + ); + } + return value; + } + const rl = createInterface({ input: process.stdin, output: process.stderr, @@ -10,7 +59,6 @@ export async function promptSecret(message: string): Promise { return new Promise((resolve) => { process.stderr.write(message); - const stdin = process.stdin; const wasRaw = stdin.isRaw; if (stdin.isTTY && stdin.setRawMode) { stdin.setRawMode(true); From 20c152a4fcc2748086fd08e882e957611166533d Mon Sep 17 00:00:00 2001 From: I4cTime Date: Sat, 11 Jul 2026 01:21:26 -0500 Subject: [PATCH 07/16] Dependencies: fix js-yaml advisory, all Dependabot majors + minors (#75) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): in-range updates, vite 8.1.4, fix js-yaml advisory pnpm update across in-range deps (mcp sdk, zod, eslint 9.x line, vitest, tsup, etc.), vite pin bumped 8.0.16 -> 8.1.4 (both the explicit devDependency and the pnpm override), and a js-yaml >=4.2.0 override resolving GHSA-h67p-54hq-rp68 (quadratic-complexity DoS in merge key handling; dev-scope via eslint) — js-yaml now resolves to 4.3.0. Covers Dependabot PRs #69 and #70. Co-Authored-By: Claude Fable 5 * chore(deps-dev): @types/node 25 -> 26 Typecheck and build clean. Covers Dependabot PR #73. Co-Authored-By: Claude Fable 5 * chore(deps-dev): eslint 9 -> 10, fix findings from newly-default rules Flat config works unchanged. ESLint 10 enables no-useless-assignment and preserve-caught-error, which found three real issues — fixed the code instead of disabling the rules: - envelope.ts: timeRemaining was assigned null then unconditionally reassigned on every path; initialize with the fallback instead - provision.ts (x2): rethrown provision errors now attach the original error as { cause } so the underlying AWS/HTTP failure isn't lost Covers Dependabot PR #72. Co-Authored-By: Claude Fable 5 * chore(deps): commander 14 -> 15 Typecheck, build, all 182 tests, and a live smoke pass: grouped help renderer, set/get/has/delete roundtrip, completion generation (bash -n clean), doctor, and unknown-command error handling all behave identically on v15. Covers Dependabot PR #71. Co-Authored-By: Claude Fable 5 * ci(deps): bump action majors — checkout v7, setup-node v6, pnpm/action-setup v6, codeql-action v4 Applied across all five workflows (including the ones PR #74 added, so the Dependabot PRs against the old files would have conflicted). Clears the Node 20 deprecation warnings the v4-generation actions emit on current runners. Covers Dependabot PRs #60, #61, #62, #63. Co-Authored-By: Claude Fable 5 * docs: changelog for dependency + security updates Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- .github/workflows/ci.yml | 24 +- .github/workflows/codeql.yml | 8 +- .github/workflows/publish.yml | 6 +- .github/workflows/release.yml | 2 +- .github/workflows/update-homebrew.yml | 2 +- CHANGELOG.md | 5 + package.json | 29 +- pnpm-lock.yaml | 1414 +++++++++++-------------- src/core/dashboard-client.ts | 4 +- src/core/envelope.ts | 4 +- src/core/provision.ts | 4 +- 11 files changed, 684 insertions(+), 818 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d816669..0fd16a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,13 +18,13 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 # pnpm version is pinned via the "packageManager" field in package.json so # CI resolves identically to local (no surprise pnpm-latest policy drift). - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: "22" cache: pnpm @@ -71,11 +71,11 @@ jobs: parity: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: "22" cache: pnpm @@ -108,11 +108,11 @@ jobs: - { os: macos-latest, node: 22, tests: true } - { os: windows-latest, node: 22, tests: true } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} cache: pnpm @@ -143,11 +143,11 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: "22" cache: pnpm diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6f52972..ff86fae 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,17 +21,17 @@ jobs: language: [javascript-typescript] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@v4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 01aa6f5..0f46509 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -36,16 +36,16 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: ref: ${{ inputs.ref || github.ref }} # pnpm version comes from the "packageManager" field in package.json; # do not also pin "version" here (pnpm/action-setup errors on conflict). # Must run before setup-node so `cache: pnpm` can find the pnpm binary. - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: # Node 24 bundles npm 11.5+, which is the minimum required for # OIDC trusted publishing on npmjs.com. Node 22 ships npm 10.x, diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 39b88db..dca5d9c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: github-release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Extract release notes from CHANGELOG run: | diff --git a/.github/workflows/update-homebrew.yml b/.github/workflows/update-homebrew.yml index 3b9d945..b82b06b 100644 --- a/.github/workflows/update-homebrew.yml +++ b/.github/workflows/update-homebrew.yml @@ -19,7 +19,7 @@ jobs: update-formula: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Get release version id: version diff --git a/CHANGELOG.md b/CHANGELOG.md index 2473659..0046cc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Dependencies +- **Fixed GHSA-h67p-54hq-rp68** (js-yaml quadratic-complexity DoS, dev-scope via eslint) with a `js-yaml >=4.2.0` override — resolves to 4.3.0. +- **Majors:** `commander` 14→15 (CLI verified end-to-end), `eslint` 9→10 (three findings from newly-default rules fixed in code), `@types/node` 25→26; GitHub Actions `checkout` v7, `setup-node` v6, `pnpm/action-setup` v6, `codeql-action` v4. +- **Minors:** in-range `pnpm update` across prod + dev deps; `vite` pin 8.0.16→8.1.4. + ### Added - **`qring has `** — script-friendly existence check (exit 0 if present, 1 if not; `--quiet` for exit-code-only, honors `--json`). Decay-aware like the MCP `has_secret` tool, closing the last MCP→CLI parity gap. - **Claude Code plugin is now a real installable plugin** — added `claude-code-plugin/.claude-plugin/plugin.json` and a repo-root `.claude-plugin/marketplace.json`, so it installs via `/plugin marketplace add I4cTime/quantum_ring` + `/plugin install qring@q-ring` instead of file copying. Plugin components (agents/commands/skills/hooks) moved from `claude-code-plugin/.claude/` to the plugin root to match the Claude Code plugin format; hooks are wired via `hooks/hooks.json` (`${CLAUDE_PLUGIN_ROOT}`) for plugin installs and `.claude/settings.json` for project-scoped sync installs. `sync-versions` now stamps both new manifests. diff --git a/package.json b/package.json index 62cbd06..4d3e0c2 100644 --- a/package.json +++ b/package.json @@ -57,24 +57,24 @@ }, "homepage": "https://qring.i4c.studio", "dependencies": { - "@modelcontextprotocol/sdk": "^1.27.1", - "@napi-rs/keyring": "^1.2.0", - "commander": "^14.0.3", - "zod": "^4.3.6" + "@modelcontextprotocol/sdk": "^1.29.0", + "@napi-rs/keyring": "^1.3.0", + "commander": "^15.0.0", + "zod": "^4.4.3" }, "devDependencies": { - "@eslint/js": "^9.39.2", - "@types/node": "^25.5.0", + "@eslint/js": "^10.0.1", + "@types/node": "^26.1.1", "esbuild": "^0.28.1", - "eslint": "^9.39.2", + "eslint": "^10.7.0", "htm": "^3.1.1", - "preact": "^10.29.2", - "prettier": "^3.7.4", + "preact": "^10.29.7", + "prettier": "^3.9.5", "tsup": "^8.5.1", "typescript": "^5.9.3", - "typescript-eslint": "^8.50.1", - "vite": "8.0.16", - "vitest": "^4.1.1" + "typescript-eslint": "^8.63.0", + "vite": "8.1.4", + "vitest": "^4.1.10" }, "pnpm": { "overrides": { @@ -82,12 +82,13 @@ "path-to-regexp": ">=8.4.0", "hono": ">=4.12.25", "@hono/node-server": ">=1.19.13", - "vite": "8.0.16", + "vite": "8.1.4", "qs": "6.15.2", "ip-address": ">=10.1.1", "fast-uri": ">=3.1.2 <4", "esbuild": ">=0.28.1", - "postcss": ">=8.5.10" + "postcss": ">=8.5.10", + "js-yaml": ">=4.2.0" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 431e39a..64f821d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,77 +9,78 @@ overrides: path-to-regexp: '>=8.4.0' hono: '>=4.12.25' '@hono/node-server': '>=1.19.13' - vite: 8.0.16 + vite: 8.1.4 qs: 6.15.2 ip-address: '>=10.1.1' fast-uri: '>=3.1.2 <4' esbuild: '>=0.28.1' postcss: '>=8.5.10' + js-yaml: '>=4.2.0' importers: .: dependencies: '@modelcontextprotocol/sdk': - specifier: ^1.27.1 - version: 1.29.0(zod@4.3.6) + specifier: ^1.29.0 + version: 1.29.0(zod@4.4.3) '@napi-rs/keyring': - specifier: ^1.2.0 - version: 1.2.0 + specifier: ^1.3.0 + version: 1.3.0 commander: - specifier: ^14.0.3 - version: 14.0.3 + specifier: ^15.0.0 + version: 15.0.0 zod: - specifier: ^4.3.6 - version: 4.3.6 + specifier: ^4.4.3 + version: 4.4.3 devDependencies: '@eslint/js': - specifier: ^9.39.2 - version: 9.39.4 + specifier: ^10.0.1 + version: 10.0.1(eslint@10.7.0) '@types/node': - specifier: ^25.5.0 - version: 25.5.2 + specifier: ^26.1.1 + version: 26.1.1 esbuild: specifier: '>=0.28.1' version: 0.28.1 eslint: - specifier: ^9.39.2 - version: 9.39.4 + specifier: ^10.7.0 + version: 10.7.0 htm: specifier: ^3.1.1 version: 3.1.1 preact: - specifier: ^10.29.2 - version: 10.29.2 + specifier: ^10.29.7 + version: 10.29.7 prettier: - specifier: ^3.7.4 - version: 3.8.3 + specifier: ^3.9.5 + version: 3.9.5 tsup: specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.15)(typescript@5.9.3) + version: 8.5.1(postcss@8.5.16)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.50.1 - version: 8.59.0(eslint@9.39.4)(typescript@5.9.3) + specifier: ^8.63.0 + version: 8.63.0(eslint@10.7.0)(typescript@5.9.3) vite: - specifier: 8.0.16 - version: 8.0.16(@types/node@25.5.2)(esbuild@0.28.1) + specifier: 8.1.4 + version: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1) vitest: - specifier: ^4.1.1 - version: 4.1.4(@types/node@25.5.2)(vite@8.0.16(@types/node@25.5.2)(esbuild@0.28.1)) + specifier: ^4.1.10 + version: 4.1.10(@types/node@26.1.1)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)) packages: - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} @@ -247,37 +248,38 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.21.2': - resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/eslintrc@3.3.5': - resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/js@9.39.4': - resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@hono/node-server@1.19.13': - resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==} - engines: {node: '>=18.14.1'} + '@hono/node-server@2.0.8': + resolution: {integrity: sha512-GuCWzLxwg218fy1JaHculFsdcuY12hxit83V+algozTPnwhNjLrRL/Alg9OYjLZLoUZ1rw/S4CdTMsnkSKCmFA==} + engines: {node: '>=20'} peerDependencies: hono: '>=4.12.25' @@ -324,85 +326,85 @@ packages: '@cfworker/json-schema': optional: true - '@napi-rs/keyring-darwin-arm64@1.2.0': - resolution: {integrity: sha512-CA83rDeyONDADO25JLZsh3eHY8yTEtm/RS6ecPsY+1v+dSawzT9GywBMu2r6uOp1IEhQs/xAfxgybGAFr17lSA==} + '@napi-rs/keyring-darwin-arm64@1.3.0': + resolution: {integrity: sha512-pl76hJvdYUBn6I24bXiOBMA9nbDapo3I5B+f3OorjDU4dUMSypXeKbOVehJe8fhgTiH24flMyTS3aAIy43xegQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@napi-rs/keyring-darwin-x64@1.2.0': - resolution: {integrity: sha512-dBHjtKRCj4ByfnfqIKIJLo3wueQNJhLRyuxtX/rR4K/XtcS7VLlRD01XXizjpre54vpmObj63w+ZpHG+mGM8uA==} + '@napi-rs/keyring-darwin-x64@1.3.0': + resolution: {integrity: sha512-YcJtEV5LA3cvA4z3BurgxH5IhTsW1JfIvcAAcqcecwk06Si9F9NqkxbZVIfDwQ8oRHgaBmT3zZJnLAotCrVahw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@napi-rs/keyring-freebsd-x64@1.2.0': - resolution: {integrity: sha512-DPZFr11pNJSnaoh0dzSUNF+T6ORhy3CkzUT3uGixbA71cAOPJ24iG8e8QrLOkuC/StWrAku3gBnth2XMWOcR3Q==} + '@napi-rs/keyring-freebsd-x64@1.3.0': + resolution: {integrity: sha512-vlLf31TGhfRAaxLDBhg8b89ss0HHD/lyNmL5F3UjSaz5CUXElsJmKYq9fqA/B+cZKUEUcLHHGhF0I/CqcFdaVw==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@napi-rs/keyring-linux-arm-gnueabihf@1.2.0': - resolution: {integrity: sha512-8xv6DyEMlvRdqJzp4F39RLUmmTQsLcGYYv/3eIfZNZN1O5257tHxTrFYqAsny659rJJK2EKeSa7PhrSibQqRWQ==} + '@napi-rs/keyring-linux-arm-gnueabihf@1.3.0': + resolution: {integrity: sha512-KiWdMMu/Inz/bHHIAGrnF7r54FZDYXuHO6UFF/rhIrshUsxbMG1Rl9lEymNtqqsVo927G0VYcb02FzWQ3iBQRQ==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@napi-rs/keyring-linux-arm64-gnu@1.2.0': - resolution: {integrity: sha512-Pu2V6Py+PBt7inryEecirl+t+ti8bhZphjP+W68iVaXHUxLdWmkgL9KI1VkbRHbx5k8K5Tew9OP218YfmVguIA==} + '@napi-rs/keyring-linux-arm64-gnu@1.3.0': + resolution: {integrity: sha512-eyKGpY40lm9Jvs1aD294XRH4y7+TlJM0YVAryZeXA6TX0mb4gMkxVXwSQv7MCwgah7raeUd0dKUb4BPAYIgcMg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@napi-rs/keyring-linux-arm64-musl@1.2.0': - resolution: {integrity: sha512-8TDymrpC4P1a9iDEaegT7RnrkmrJN5eNZh3Im3UEV5PPYGtrb82CRxsuFohthCWQW81O483u1bu+25+XA4nKUw==} + '@napi-rs/keyring-linux-arm64-musl@1.3.0': + resolution: {integrity: sha512-iIK6JWHXAJqDrEyLY3TmswwloVyt2vj+04TZnew+uSJ9gnDO8EwRbp3/iw3LpWaXiDO7VomGO6y8I0Id8uBZSw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@napi-rs/keyring-linux-riscv64-gnu@1.2.0': - resolution: {integrity: sha512-awsB5XI1MYL7fwfjMDGmKOWvNgJEO7mM7iVEMS0fO39f0kVJnOSjlu7RHcXAF0LOx+0VfF3oxbWqJmZbvRCRHw==} + '@napi-rs/keyring-linux-riscv64-gnu@1.3.0': + resolution: {integrity: sha512-/PGqrwn6EwgtK6vccASSXJRfOSP4vN1F4ASsIQ+7MdrK6hNvAJ1FZPrIuD5gGGdxezo3F++To2Wq7DbuGIeuNQ==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] libc: [glibc] - '@napi-rs/keyring-linux-x64-gnu@1.2.0': - resolution: {integrity: sha512-8E+7z4tbxSJXxIBqA+vfB1CGajpCDRyTyqXkBig5NtASrv4YXcntSo96Iah2QDR5zD3dSTsmbqJudcj9rKKuHQ==} + '@napi-rs/keyring-linux-x64-gnu@1.3.0': + resolution: {integrity: sha512-2PDK1WKWTu9lBGq9VvNEkSlQD3O7YwVpmnyN2M3cy4v7NJ/8gDMd9GXv3G+FVXN13uhp4gnnPBS+ScefmEeD2A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@napi-rs/keyring-linux-x64-musl@1.2.0': - resolution: {integrity: sha512-8RZ8yVEnmWr/3BxKgBSzmgntI7lNEsY7xouNfOsQkuVAiCNmxzJwETspzK3PQ2FHtDxgz5vHQDEBVGMyM4hUHA==} + '@napi-rs/keyring-linux-x64-musl@1.3.0': + resolution: {integrity: sha512-oJ2HkX8YUo46QBkn0pG+HuIKQNqr523q6vBobCn+P95s4C4K6/kLBqHY/1bg5J4ap31DzsznhnFKcfBNBsjCnw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@napi-rs/keyring-win32-arm64-msvc@1.2.0': - resolution: {integrity: sha512-AoqaDZpQ6KPE19VBLpxyORcp+yWmHI9Xs9Oo0PJ4mfHma4nFSLVdhAubJCxdlNptHe5va7ghGCHj3L9Akiv4cQ==} + '@napi-rs/keyring-win32-arm64-msvc@1.3.0': + resolution: {integrity: sha512-tOd3c/uAaeoE4ycVlmAdSvygz0Zt3zdca6Y7gokBeIbaRDWpjDIUOpU3MvML59XAaqyuKGsVVu0F/DZb1lHPmw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@napi-rs/keyring-win32-ia32-msvc@1.2.0': - resolution: {integrity: sha512-EYL+EEI6bCsYi3LfwcQdnX3P/R76ENKNn+3PmpGheBsUFLuh0gQuP7aMVHM4rTw6UVe+L3vCLZSptq/oeacz0A==} + '@napi-rs/keyring-win32-ia32-msvc@1.3.0': + resolution: {integrity: sha512-sPSqeAFZMGqP1R++M2JTza7GQJJ/TpCo6JU6Vcd4jnebvOaEDs9b7eipakU1PJdSvhpC2yXMCNRk9gXfrhuwHQ==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@napi-rs/keyring-win32-x64-msvc@1.2.0': - resolution: {integrity: sha512-xFlx/TsmqmCwNU9v+AVnEJgoEAlBYgzFF5Ihz1rMpPAt4qQWWkMd4sCyM1gMJ1A/GnRqRegDiQpwaxGUHFtFbA==} + '@napi-rs/keyring-win32-x64-msvc@1.3.0': + resolution: {integrity: sha512-4DnCWXwDc0HRKwyRlG5y0VhKZW2tNRQfKKfyj6IX/KWfDNyq9hn4n+GL1auyDcOO/v8PwnhmYo2+rOOqCkvvOg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@napi-rs/keyring@1.2.0': - resolution: {integrity: sha512-d0d4Oyxm+v980PEq1ZH2PmS6cvpMIRc17eYpiU47KgW+lzxklMu6+HOEOPmxrpnF/XQZ0+Q78I2mgMhbIIo/dg==} + '@napi-rs/keyring@1.3.0': + resolution: {integrity: sha512-WrOw/bcXm0f9qHkumlT1QlArXSTWqaY9sunsDpOk+yCCorCKMxvWT/a3xko4EYHVdeZoh00yI2TydXn6eyICDA==} engines: {node: '>= 10'} '@napi-rs/wasm-runtime@1.1.6': @@ -411,100 +413,100 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -512,141 +514,141 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - '@rollup/rollup-android-arm-eabi@4.60.1': - resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.60.1': - resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.60.1': - resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.60.1': - resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.60.1': - resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.60.1': - resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.60.1': - resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.60.1': - resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.60.1': - resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.60.1': - resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.60.1': - resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.60.1': - resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.60.1': - resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.60.1': - resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.60.1': - resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.60.1': - resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.60.1': - resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.60.1': - resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.60.1': - resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.60.1': - resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.60.1': - resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} cpu: [x64] os: [win32] @@ -662,102 +664,105 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@25.5.2': - resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} + '@types/node@26.1.1': + resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} - '@typescript-eslint/eslint-plugin@8.59.0': - resolution: {integrity: sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==} + '@typescript-eslint/eslint-plugin@8.63.0': + resolution: {integrity: sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.59.0 + '@typescript-eslint/parser': ^8.63.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.59.0': - resolution: {integrity: sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==} + '@typescript-eslint/parser@8.63.0': + resolution: {integrity: sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.59.0': - resolution: {integrity: sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==} + '@typescript-eslint/project-service@8.63.0': + resolution: {integrity: sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.59.0': - resolution: {integrity: sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==} + '@typescript-eslint/scope-manager@8.63.0': + resolution: {integrity: sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.59.0': - resolution: {integrity: sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==} + '@typescript-eslint/tsconfig-utils@8.63.0': + resolution: {integrity: sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.59.0': - resolution: {integrity: sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==} + '@typescript-eslint/type-utils@8.63.0': + resolution: {integrity: sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.59.0': - resolution: {integrity: sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==} + '@typescript-eslint/types@8.63.0': + resolution: {integrity: sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.59.0': - resolution: {integrity: sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==} + '@typescript-eslint/typescript-estree@8.63.0': + resolution: {integrity: sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.59.0': - resolution: {integrity: sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==} + '@typescript-eslint/utils@8.63.0': + resolution: {integrity: sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.59.0': - resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} + '@typescript-eslint/visitor-keys@8.63.0': + resolution: {integrity: sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitest/expect@4.1.4': - resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - '@vitest/mocker@4.1.4': - resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==} + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: msw: ^2.4.9 - vite: 8.0.16 + vite: 8.1.4 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@4.1.4': - resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==} + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - '@vitest/runner@4.1.4': - resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==} + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - '@vitest/snapshot@4.1.4': - resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==} + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - '@vitest/spy@4.1.4': - resolution: {integrity: sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==} + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - '@vitest/utils@4.1.4': - resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} @@ -768,8 +773,8 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} hasBin: true @@ -784,39 +789,26 @@ packages: ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - ajv@8.18.0: - resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - body-parser@2.2.2: - resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} - brace-expansion@1.1.14: - resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} - - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} engines: {node: 18 || 20 || >=22} bundle-require@5.1.0: @@ -841,40 +833,22 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - commander@14.0.3: - resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} - engines: {node: '>=20'} + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -890,6 +864,10 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -948,11 +926,11 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@2.0.0: - resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-module-lexer@2.3.0: + resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} esbuild@0.28.1: @@ -967,25 +945,21 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-visitor-keys@5.0.1: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@9.39.4: - resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@10.7.0: + resolution: {integrity: sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: jiti: '*' @@ -993,9 +967,9 @@ packages: jiti: optional: true - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} esquery@1.7.0: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} @@ -1020,20 +994,20 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - eventsource-parser@3.0.6: - resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} engines: {node: '>=18.0.0'} eventsource@3.0.7: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} - express-rate-limit@8.3.2: - resolution: {integrity: sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==} + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' @@ -1051,8 +1025,8 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -1113,28 +1087,20 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} - hono@4.12.27: - resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==} + hono@4.12.29: + resolution: {integrity: sha512-1hNiRjawYrLq/4m3DQQjPGFg0VZkk4RjQJDff/excI6Dm9BiL75qxGrd7/c6YOxPdq6AscP3LiXhQ6fKFC1Waw==} engines: {node: '>=16.9.0'} htm@3.1.1: @@ -1144,22 +1110,18 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} engines: {node: '>= 4'} - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -1189,17 +1151,13 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jose@6.2.2: - resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -1311,9 +1269,6 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1341,9 +1296,6 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} - minimatch@3.1.5: - resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -1373,8 +1325,9 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} @@ -1395,10 +1348,6 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -1420,8 +1369,8 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} pirates@4.0.7: @@ -1453,19 +1402,24 @@ packages: yaml: optional: true - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} - preact@10.29.2: - resolution: {integrity: sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==} + preact@10.29.7: + resolution: {integrity: sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q==} + peerDependencies: + preact-render-to-string: '>=5' + peerDependenciesMeta: + preact-render-to-string: + optional: true prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier@3.8.3: - resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + prettier@3.9.5: + resolution: {integrity: sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==} engines: {node: '>=14'} hasBin: true @@ -1481,8 +1435,8 @@ packages: resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} engines: {node: '>= 0.6'} raw-body@3.0.2: @@ -1497,21 +1451,17 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rollup@4.60.1: - resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1522,8 +1472,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true @@ -1558,8 +1508,8 @@ packages: resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} engines: {node: '>= 0.4'} - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} siginfo@2.0.0: @@ -1580,22 +1530,14 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@4.0.0: - resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -1609,14 +1551,10 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.1.1: - resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -1668,12 +1606,12 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} - typescript-eslint@8.59.0: - resolution: {integrity: sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==} + typescript-eslint@8.63.0: + resolution: {integrity: sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -1684,11 +1622,11 @@ packages: engines: {node: '>=14.17'} hasBin: true - ufo@1.6.3: - resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} @@ -1701,13 +1639,13 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vite@8.0.16: - resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + vite@8.1.4: + resolution: {integrity: sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 + '@vitejs/devtools': ^0.3.0 esbuild: '>=0.28.1' jiti: '>=1.21.0' less: ^4.0.0 @@ -1744,23 +1682,23 @@ packages: yaml: optional: true - vitest@4.1.4: - resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==} + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.4 - '@vitest/browser-preview': 4.1.4 - '@vitest/browser-webdriverio': 4.1.4 - '@vitest/coverage-istanbul': 4.1.4 - '@vitest/coverage-v8': 4.1.4 - '@vitest/ui': 4.1.4 + '@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: 8.0.16 + vite: 8.1.4 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -1811,23 +1749,23 @@ packages: peerDependencies: zod: ^3.25.28 || ^4 - zod@4.3.6: - resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} snapshots: - '@emnapi/core@1.10.0': + '@emnapi/core@1.11.1': dependencies: - '@emnapi/wasi-threads': 1.2.1 + '@emnapi/wasi-threads': 1.2.2 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.10.0': + '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.1': + '@emnapi/wasi-threads@1.2.2': dependencies: tslib: 2.8.1 optional: true @@ -1910,55 +1848,43 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.7.0)': dependencies: - eslint: 9.39.4 + eslint: 10.7.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.2': + '@eslint/config-array@0.23.5': dependencies: - '@eslint/object-schema': 2.1.7 + '@eslint/object-schema': 3.0.5 debug: 4.4.3 - minimatch: 3.1.5 + minimatch: 10.2.5 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.4.2': + '@eslint/config-helpers@0.6.0': dependencies: - '@eslint/core': 0.17.0 + '@eslint/core': 1.2.1 - '@eslint/core@0.17.0': + '@eslint/core@1.2.1': dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.5': - dependencies: - ajv: 6.15.0 - debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.39.4': {} + '@eslint/js@10.0.1(eslint@10.7.0)': + optionalDependencies: + eslint: 10.7.0 - '@eslint/object-schema@2.1.7': {} + '@eslint/object-schema@3.0.5': {} - '@eslint/plugin-kit@0.4.1': + '@eslint/plugin-kit@0.7.2': dependencies: - '@eslint/core': 0.17.0 + '@eslint/core': 1.2.1 levn: 0.4.1 - '@hono/node-server@1.19.13(hono@4.12.27)': + '@hono/node-server@2.0.8(hono@4.12.29)': dependencies: - hono: 4.12.27 + hono: 4.12.29 '@humanfs/core@0.19.2': dependencies: @@ -1990,212 +1916,212 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)': + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': dependencies: - '@hono/node-server': 1.19.13(hono@4.12.27) - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) + '@hono/node-server': 2.0.8(hono@4.12.29) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 cors: 2.8.6 cross-spawn: 7.0.6 eventsource: 3.0.7 - eventsource-parser: 3.0.6 + eventsource-parser: 3.1.0 express: 5.2.1 - express-rate-limit: 8.3.2(express@5.2.1) - hono: 4.12.27 - jose: 6.2.2 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.29 + jose: 6.2.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 raw-body: 3.0.2 - zod: 4.3.6 - zod-to-json-schema: 3.25.2(zod@4.3.6) + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) transitivePeerDependencies: - supports-color - '@napi-rs/keyring-darwin-arm64@1.2.0': + '@napi-rs/keyring-darwin-arm64@1.3.0': optional: true - '@napi-rs/keyring-darwin-x64@1.2.0': + '@napi-rs/keyring-darwin-x64@1.3.0': optional: true - '@napi-rs/keyring-freebsd-x64@1.2.0': + '@napi-rs/keyring-freebsd-x64@1.3.0': optional: true - '@napi-rs/keyring-linux-arm-gnueabihf@1.2.0': + '@napi-rs/keyring-linux-arm-gnueabihf@1.3.0': optional: true - '@napi-rs/keyring-linux-arm64-gnu@1.2.0': + '@napi-rs/keyring-linux-arm64-gnu@1.3.0': optional: true - '@napi-rs/keyring-linux-arm64-musl@1.2.0': + '@napi-rs/keyring-linux-arm64-musl@1.3.0': optional: true - '@napi-rs/keyring-linux-riscv64-gnu@1.2.0': + '@napi-rs/keyring-linux-riscv64-gnu@1.3.0': optional: true - '@napi-rs/keyring-linux-x64-gnu@1.2.0': + '@napi-rs/keyring-linux-x64-gnu@1.3.0': optional: true - '@napi-rs/keyring-linux-x64-musl@1.2.0': + '@napi-rs/keyring-linux-x64-musl@1.3.0': optional: true - '@napi-rs/keyring-win32-arm64-msvc@1.2.0': + '@napi-rs/keyring-win32-arm64-msvc@1.3.0': optional: true - '@napi-rs/keyring-win32-ia32-msvc@1.2.0': + '@napi-rs/keyring-win32-ia32-msvc@1.3.0': optional: true - '@napi-rs/keyring-win32-x64-msvc@1.2.0': + '@napi-rs/keyring-win32-x64-msvc@1.3.0': optional: true - '@napi-rs/keyring@1.2.0': + '@napi-rs/keyring@1.3.0': optionalDependencies: - '@napi-rs/keyring-darwin-arm64': 1.2.0 - '@napi-rs/keyring-darwin-x64': 1.2.0 - '@napi-rs/keyring-freebsd-x64': 1.2.0 - '@napi-rs/keyring-linux-arm-gnueabihf': 1.2.0 - '@napi-rs/keyring-linux-arm64-gnu': 1.2.0 - '@napi-rs/keyring-linux-arm64-musl': 1.2.0 - '@napi-rs/keyring-linux-riscv64-gnu': 1.2.0 - '@napi-rs/keyring-linux-x64-gnu': 1.2.0 - '@napi-rs/keyring-linux-x64-musl': 1.2.0 - '@napi-rs/keyring-win32-arm64-msvc': 1.2.0 - '@napi-rs/keyring-win32-ia32-msvc': 1.2.0 - '@napi-rs/keyring-win32-x64-msvc': 1.2.0 - - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 + '@napi-rs/keyring-darwin-arm64': 1.3.0 + '@napi-rs/keyring-darwin-x64': 1.3.0 + '@napi-rs/keyring-freebsd-x64': 1.3.0 + '@napi-rs/keyring-linux-arm-gnueabihf': 1.3.0 + '@napi-rs/keyring-linux-arm64-gnu': 1.3.0 + '@napi-rs/keyring-linux-arm64-musl': 1.3.0 + '@napi-rs/keyring-linux-riscv64-gnu': 1.3.0 + '@napi-rs/keyring-linux-x64-gnu': 1.3.0 + '@napi-rs/keyring-linux-x64-musl': 1.3.0 + '@napi-rs/keyring-win32-arm64-msvc': 1.3.0 + '@napi-rs/keyring-win32-ia32-msvc': 1.3.0 + '@napi-rs/keyring-win32-x64-msvc': 1.3.0 + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 '@tybys/wasm-util': 0.10.3 optional: true - '@oxc-project/types@0.133.0': {} + '@oxc-project/types@0.139.0': {} - '@rolldown/binding-android-arm64@1.0.3': + '@rolldown/binding-android-arm64@1.1.5': optional: true - '@rolldown/binding-darwin-arm64@1.0.3': + '@rolldown/binding-darwin-arm64@1.1.5': optional: true - '@rolldown/binding-darwin-x64@1.0.3': + '@rolldown/binding-darwin-x64@1.1.5': optional: true - '@rolldown/binding-freebsd-x64@1.0.3': + '@rolldown/binding-freebsd-x64@1.1.5': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.3': + '@rolldown/binding-linux-arm64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.3': + '@rolldown/binding-linux-arm64-musl@1.1.5': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.3': + '@rolldown/binding-linux-ppc64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.3': + '@rolldown/binding-linux-s390x-gnu@1.1.5': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.3': + '@rolldown/binding-linux-x64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-x64-musl@1.0.3': + '@rolldown/binding-linux-x64-musl@1.1.5': optional: true - '@rolldown/binding-openharmony-arm64@1.0.3': + '@rolldown/binding-openharmony-arm64@1.1.5': optional: true - '@rolldown/binding-wasm32-wasi@1.0.3': + '@rolldown/binding-wasm32-wasi@1.1.5': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.3': + '@rolldown/binding-win32-arm64-msvc@1.1.5': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.3': + '@rolldown/binding-win32-x64-msvc@1.1.5': optional: true '@rolldown/pluginutils@1.0.1': {} - '@rollup/rollup-android-arm-eabi@4.60.1': + '@rollup/rollup-android-arm-eabi@4.62.2': optional: true - '@rollup/rollup-android-arm64@4.60.1': + '@rollup/rollup-android-arm64@4.62.2': optional: true - '@rollup/rollup-darwin-arm64@4.60.1': + '@rollup/rollup-darwin-arm64@4.62.2': optional: true - '@rollup/rollup-darwin-x64@4.60.1': + '@rollup/rollup-darwin-x64@4.62.2': optional: true - '@rollup/rollup-freebsd-arm64@4.60.1': + '@rollup/rollup-freebsd-arm64@4.62.2': optional: true - '@rollup/rollup-freebsd-x64@4.60.1': + '@rollup/rollup-freebsd-x64@4.62.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.60.1': + '@rollup/rollup-linux-arm-musleabihf@4.62.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.60.1': + '@rollup/rollup-linux-arm64-gnu@4.62.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.60.1': + '@rollup/rollup-linux-arm64-musl@4.62.2': optional: true - '@rollup/rollup-linux-loong64-gnu@4.60.1': + '@rollup/rollup-linux-loong64-gnu@4.62.2': optional: true - '@rollup/rollup-linux-loong64-musl@4.60.1': + '@rollup/rollup-linux-loong64-musl@4.62.2': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.1': + '@rollup/rollup-linux-ppc64-gnu@4.62.2': optional: true - '@rollup/rollup-linux-ppc64-musl@4.60.1': + '@rollup/rollup-linux-ppc64-musl@4.62.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.1': + '@rollup/rollup-linux-riscv64-gnu@4.62.2': optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.1': + '@rollup/rollup-linux-riscv64-musl@4.62.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.1': + '@rollup/rollup-linux-s390x-gnu@4.62.2': optional: true - '@rollup/rollup-linux-x64-gnu@4.60.1': + '@rollup/rollup-linux-x64-gnu@4.62.2': optional: true - '@rollup/rollup-linux-x64-musl@4.60.1': + '@rollup/rollup-linux-x64-musl@4.62.2': optional: true - '@rollup/rollup-openbsd-x64@4.60.1': + '@rollup/rollup-openbsd-x64@4.62.2': optional: true - '@rollup/rollup-openharmony-arm64@4.60.1': + '@rollup/rollup-openharmony-arm64@4.62.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.60.1': + '@rollup/rollup-win32-arm64-msvc@4.62.2': optional: true - '@rollup/rollup-win32-ia32-msvc@4.60.1': + '@rollup/rollup-win32-ia32-msvc@4.62.2': optional: true - '@rollup/rollup-win32-x64-gnu@4.60.1': + '@rollup/rollup-win32-x64-gnu@4.62.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.60.1': + '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true '@standard-schema/spec@1.1.0': {} @@ -2212,143 +2138,145 @@ snapshots: '@types/deep-eql@4.0.2': {} - '@types/estree@1.0.8': {} + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} '@types/json-schema@7.0.15': {} - '@types/node@25.5.2': + '@types/node@26.1.1': dependencies: - undici-types: 7.18.2 + undici-types: 8.3.0 - '@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.7.0)(typescript@5.9.3))(eslint@10.7.0)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.59.0(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.59.0 - '@typescript-eslint/type-utils': 8.59.0(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.0(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.59.0 - eslint: 9.39.4 - ignore: 7.0.5 + '@typescript-eslint/parser': 8.63.0(eslint@10.7.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.63.0 + '@typescript-eslint/type-utils': 8.63.0(eslint@10.7.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.63.0(eslint@10.7.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.63.0 + eslint: 10.7.0 + ignore: 7.0.6 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/parser@8.63.0(eslint@10.7.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.59.0 - '@typescript-eslint/types': 8.59.0 - '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.59.0 + '@typescript-eslint/scope-manager': 8.63.0 + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.63.0 debug: 4.4.3 - eslint: 9.39.4 + eslint: 10.7.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.59.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.63.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@5.9.3) - '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@5.9.3) + '@typescript-eslint/types': 8.63.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.59.0': + '@typescript-eslint/scope-manager@8.63.0': dependencies: - '@typescript-eslint/types': 8.59.0 - '@typescript-eslint/visitor-keys': 8.59.0 + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/visitor-keys': 8.63.0 - '@typescript-eslint/tsconfig-utils@8.59.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.63.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.59.0(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.63.0(eslint@10.7.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.59.0 - '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.63.0(eslint@10.7.0)(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.4 + eslint: 10.7.0 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.59.0': {} + '@typescript-eslint/types@8.63.0': {} - '@typescript-eslint/typescript-estree@8.59.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.63.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.59.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@5.9.3) - '@typescript-eslint/types': 8.59.0 - '@typescript-eslint/visitor-keys': 8.59.0 + '@typescript-eslint/project-service': 8.63.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@5.9.3) + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/visitor-keys': 8.63.0 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.7.4 - tinyglobby: 0.2.16 + semver: 7.8.5 + tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.59.0(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/utils@8.63.0(eslint@10.7.0)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) - '@typescript-eslint/scope-manager': 8.59.0 - '@typescript-eslint/types': 8.59.0 - '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) - eslint: 9.39.4 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0) + '@typescript-eslint/scope-manager': 8.63.0 + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) + eslint: 10.7.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.59.0': + '@typescript-eslint/visitor-keys@8.63.0': dependencies: - '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/types': 8.63.0 eslint-visitor-keys: 5.0.1 - '@vitest/expect@4.1.4': + '@vitest/expect@4.1.10': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.4 - '@vitest/utils': 4.1.4 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.4(vite@8.0.16(@types/node@25.5.2)(esbuild@0.28.1))': + '@vitest/mocker@4.1.10(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1))': dependencies: - '@vitest/spy': 4.1.4 + '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.16(@types/node@25.5.2)(esbuild@0.28.1) + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1) - '@vitest/pretty-format@4.1.4': + '@vitest/pretty-format@4.1.10': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.4': + '@vitest/runner@4.1.10': dependencies: - '@vitest/utils': 4.1.4 + '@vitest/utils': 4.1.10 pathe: 2.0.3 - '@vitest/snapshot@4.1.4': + '@vitest/snapshot@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.4 - '@vitest/utils': 4.1.4 + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.4': {} + '@vitest/spy@4.1.10': {} - '@vitest/utils@4.1.4': + '@vitest/utils@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.4 + '@vitest/pretty-format': 4.1.10 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -2357,15 +2285,15 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn-jsx@5.3.2(acorn@8.16.0): + acorn-jsx@5.3.2(acorn@8.17.0): dependencies: - acorn: 8.16.0 + acorn: 8.17.0 - acorn@8.16.0: {} + acorn@8.17.0: {} - ajv-formats@3.0.1(ajv@8.18.0): + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: - ajv: 8.18.0 + ajv: 8.20.0 ajv@6.15.0: dependencies: @@ -2374,47 +2302,34 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.18.0: + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 + fast-uri: 3.1.3 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - any-promise@1.3.0: {} - argparse@2.0.1: {} - assertion-error@2.0.1: {} - balanced-match@1.0.2: {} - balanced-match@4.0.4: {} - body-parser@2.2.2: + body-parser@2.3.0: dependencies: bytes: 3.1.2 - content-type: 1.0.5 + content-type: 2.0.0 debug: 4.4.3 http-errors: 2.0.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.3 on-finished: 2.4.1 qs: 6.15.2 raw-body: 3.0.2 - type-is: 2.0.1 + type-is: 2.1.0 transitivePeerDependencies: - supports-color - brace-expansion@1.1.14: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@5.0.5: + brace-expansion@5.0.7: dependencies: balanced-match: 4.0.4 @@ -2437,31 +2352,16 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - callsites@3.1.0: {} - chai@6.2.2: {} - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - chokidar@4.0.3: dependencies: readdirp: 4.1.2 - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - commander@14.0.3: {} + commander@15.0.0: {} commander@4.1.1: {} - concat-map@0.0.1: {} - confbox@0.1.8: {} consola@3.4.2: {} @@ -2470,6 +2370,8 @@ snapshots: content-type@1.0.5: {} + content-type@2.0.0: {} + convert-source-map@2.0.0: {} cookie-signature@1.2.2: {} @@ -2511,9 +2413,9 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@2.0.0: {} + es-module-lexer@2.3.0: {} - es-object-atoms@1.1.1: + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -2550,39 +2452,36 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-scope@8.4.0: + eslint-scope@9.1.2: dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.2.1: {} - eslint-visitor-keys@5.0.1: {} - eslint@9.39.4: + eslint@10.7.0: dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.2 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.5 - '@eslint/js': 9.39.4 - '@eslint/plugin-kit': 0.4.1 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 ajv: 6.15.0 - chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -2593,18 +2492,17 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.5 + minimatch: 10.2.5 natural-compare: 1.4.0 optionator: 0.9.4 transitivePeerDependencies: - supports-color - espree@10.4.0: + espree@11.2.0: dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 4.2.1 + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 5.0.1 esquery@1.7.0: dependencies: @@ -2618,21 +2516,21 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 esutils@2.0.3: {} etag@1.8.1: {} - eventsource-parser@3.0.6: {} + eventsource-parser@3.1.0: {} eventsource@3.0.7: dependencies: - eventsource-parser: 3.0.6 + eventsource-parser: 3.1.0 - expect-type@1.3.0: {} + expect-type@1.4.0: {} - express-rate-limit@8.3.2(express@5.2.1): + express-rate-limit@8.5.2(express@5.2.1): dependencies: express: 5.2.1 ip-address: 10.2.0 @@ -2640,7 +2538,7 @@ snapshots: express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.2.2 + body-parser: 2.3.0 content-disposition: 1.1.0 content-type: 1.0.5 cookie: 0.7.2 @@ -2660,12 +2558,12 @@ snapshots: parseurl: 1.3.3 proxy-addr: 2.0.7 qs: 6.15.2 - range-parser: 1.2.1 + range-parser: 1.3.0 router: 2.2.0 send: 1.2.1 serve-static: 2.2.1 statuses: 2.0.2 - type-is: 2.0.1 + type-is: 2.1.0 vary: 1.1.2 transitivePeerDependencies: - supports-color @@ -2676,11 +2574,11 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-uri@3.1.2: {} + fast-uri@3.1.3: {} - fdir@6.5.0(picomatch@4.0.4): + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: - picomatch: 4.0.4 + picomatch: 4.0.5 file-entry-cache@8.0.0: dependencies: @@ -2706,7 +2604,7 @@ snapshots: dependencies: magic-string: 0.30.21 mlly: 1.8.2 - rollup: 4.60.1 + rollup: 4.62.2 flat-cache@4.0.1: dependencies: @@ -2729,36 +2627,32 @@ snapshots: call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 function-bind: 1.1.2 get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.4 math-intrinsics: 1.1.0 get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 glob-parent@6.0.2: dependencies: is-glob: 4.0.3 - globals@14.0.0: {} - gopd@1.2.0: {} - has-flag@4.0.0: {} - has-symbols@1.1.0: {} - hasown@2.0.2: + hasown@2.0.4: dependencies: function-bind: 1.1.2 - hono@4.12.27: {} + hono@4.12.29: {} htm@3.1.1: {} @@ -2770,18 +2664,13 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - iconv-lite@0.7.2: + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 ignore@5.3.2: {} - ignore@7.0.5: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 + ignore@7.0.6: {} imurmurhash@0.1.4: {} @@ -2801,14 +2690,10 @@ snapshots: isexe@2.0.0: {} - jose@6.2.2: {} + jose@6.2.3: {} joycon@3.1.1: {} - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} @@ -2887,8 +2772,6 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash.merge@4.6.2: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2907,18 +2790,14 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.5 - - minimatch@3.1.5: - dependencies: - brace-expansion: 1.1.14 + brace-expansion: 5.0.7 mlly@1.8.2: dependencies: - acorn: 8.16.0 + acorn: 8.17.0 pathe: 2.0.3 pkg-types: 1.3.1 - ufo: 1.6.3 + ufo: 1.6.4 ms@2.1.3: {} @@ -2938,7 +2817,7 @@ snapshots: object-inspect@1.13.4: {} - obug@2.1.1: {} + obug@2.1.3: {} on-finished@2.4.1: dependencies: @@ -2965,10 +2844,6 @@ snapshots: dependencies: p-limit: 3.1.0 - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - parseurl@1.3.3: {} path-exists@4.0.0: {} @@ -2981,7 +2856,7 @@ snapshots: picocolors@1.1.1: {} - picomatch@4.0.4: {} + picomatch@4.0.5: {} pirates@4.0.7: {} @@ -2993,23 +2868,23 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 - postcss-load-config@6.0.1(postcss@8.5.15): + postcss-load-config@6.0.1(postcss@8.5.16): dependencies: lilconfig: 3.1.3 optionalDependencies: - postcss: 8.5.15 + postcss: 8.5.16 - postcss@8.5.15: + postcss@8.5.16: dependencies: nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 - preact@10.29.2: {} + preact@10.29.7: {} prelude-ls@1.2.1: {} - prettier@3.8.3: {} + prettier@3.9.5: {} proxy-addr@2.0.7: dependencies: @@ -3020,75 +2895,73 @@ snapshots: qs@6.15.2: dependencies: - side-channel: 1.1.0 + side-channel: 1.1.1 - range-parser@1.2.1: {} + range-parser@1.3.0: {} raw-body@3.0.2: dependencies: bytes: 3.1.2 http-errors: 2.0.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.3 unpipe: 1.0.0 readdirp@4.1.2: {} require-from-string@2.0.2: {} - resolve-from@4.0.0: {} - resolve-from@5.0.0: {} - rolldown@1.0.3: + rolldown@1.1.5: dependencies: - '@oxc-project/types': 0.133.0 + '@oxc-project/types': 0.139.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 - - rollup@4.60.1: - dependencies: - '@types/estree': 1.0.8 + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.1 - '@rollup/rollup-android-arm64': 4.60.1 - '@rollup/rollup-darwin-arm64': 4.60.1 - '@rollup/rollup-darwin-x64': 4.60.1 - '@rollup/rollup-freebsd-arm64': 4.60.1 - '@rollup/rollup-freebsd-x64': 4.60.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 - '@rollup/rollup-linux-arm-musleabihf': 4.60.1 - '@rollup/rollup-linux-arm64-gnu': 4.60.1 - '@rollup/rollup-linux-arm64-musl': 4.60.1 - '@rollup/rollup-linux-loong64-gnu': 4.60.1 - '@rollup/rollup-linux-loong64-musl': 4.60.1 - '@rollup/rollup-linux-ppc64-gnu': 4.60.1 - '@rollup/rollup-linux-ppc64-musl': 4.60.1 - '@rollup/rollup-linux-riscv64-gnu': 4.60.1 - '@rollup/rollup-linux-riscv64-musl': 4.60.1 - '@rollup/rollup-linux-s390x-gnu': 4.60.1 - '@rollup/rollup-linux-x64-gnu': 4.60.1 - '@rollup/rollup-linux-x64-musl': 4.60.1 - '@rollup/rollup-openbsd-x64': 4.60.1 - '@rollup/rollup-openharmony-arm64': 4.60.1 - '@rollup/rollup-win32-arm64-msvc': 4.60.1 - '@rollup/rollup-win32-ia32-msvc': 4.60.1 - '@rollup/rollup-win32-x64-gnu': 4.60.1 - '@rollup/rollup-win32-x64-msvc': 4.60.1 + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 fsevents: 2.3.3 router@2.2.0: @@ -3103,7 +2976,7 @@ snapshots: safer-buffer@2.1.2: {} - semver@7.7.4: {} + semver@7.8.5: {} send@1.2.1: dependencies: @@ -3116,7 +2989,7 @@ snapshots: mime-types: 3.0.2 ms: 2.1.3 on-finished: 2.4.1 - range-parser: 1.2.1 + range-parser: 1.3.0 statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -3158,7 +3031,7 @@ snapshots: object-inspect: 1.13.4 side-channel-map: 1.0.1 - side-channel@1.1.0: + side-channel@1.1.1: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 @@ -3176,9 +3049,7 @@ snapshots: statuses@2.0.2: {} - std-env@4.0.0: {} - - strip-json-comments@3.1.1: {} + std-env@4.2.0: {} sucrase@3.35.1: dependencies: @@ -3187,13 +3058,9 @@ snapshots: lines-and-columns: 1.2.4 mz: 2.7.0 pirates: 4.0.7 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 ts-interface-checker: 0.1.13 - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -3206,17 +3073,12 @@ snapshots: tinyexec@0.3.2: {} - tinyexec@1.1.1: {} - - tinyglobby@0.2.16: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + tinyexec@1.2.4: {} tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tinyrainbow@3.1.0: {} @@ -3233,7 +3095,7 @@ snapshots: tslib@2.8.1: optional: true - tsup@8.5.1(postcss@8.5.15)(typescript@5.9.3): + tsup@8.5.1(postcss@8.5.16)(typescript@5.9.3): dependencies: bundle-require: 5.1.0(esbuild@0.28.1) cac: 6.7.14 @@ -3244,16 +3106,16 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.15) + postcss-load-config: 6.0.1(postcss@8.5.16) resolve-from: 5.0.0 - rollup: 4.60.1 + rollup: 4.62.2 source-map: 0.7.6 sucrase: 3.35.1 tinyexec: 0.3.2 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.5.15 + postcss: 8.5.16 typescript: 5.9.3 transitivePeerDependencies: - jiti @@ -3265,28 +3127,28 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-is@2.0.1: + type-is@2.1.0: dependencies: - content-type: 1.0.5 + content-type: 2.0.0 media-typer: 1.1.0 mime-types: 3.0.2 - typescript-eslint@8.59.0(eslint@9.39.4)(typescript@5.9.3): + typescript-eslint@8.63.0(eslint@10.7.0)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/parser': 8.59.0(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.0(eslint@9.39.4)(typescript@5.9.3) - eslint: 9.39.4 + '@typescript-eslint/eslint-plugin': 8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.7.0)(typescript@5.9.3))(eslint@10.7.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.63.0(eslint@10.7.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.63.0(eslint@10.7.0)(typescript@5.9.3) + eslint: 10.7.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color typescript@5.9.3: {} - ufo@1.6.3: {} + ufo@1.6.4: {} - undici-types@7.18.2: {} + undici-types@8.3.0: {} unpipe@1.0.0: {} @@ -3296,42 +3158,42 @@ snapshots: vary@1.1.2: {} - vite@8.0.16(@types/node@25.5.2)(esbuild@0.28.1): + vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1): dependencies: lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 - rolldown: 1.0.3 + picomatch: 4.0.5 + postcss: 8.5.16 + rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.5.2 + '@types/node': 26.1.1 esbuild: 0.28.1 fsevents: 2.3.3 - vitest@4.1.4(@types/node@25.5.2)(vite@8.0.16(@types/node@25.5.2)(esbuild@0.28.1)): - dependencies: - '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.16(@types/node@25.5.2)(esbuild@0.28.1)) - '@vitest/pretty-format': 4.1.4 - '@vitest/runner': 4.1.4 - '@vitest/snapshot': 4.1.4 - '@vitest/spy': 4.1.4 - '@vitest/utils': 4.1.4 - es-module-lexer: 2.0.0 - expect-type: 1.3.0 + vitest@4.1.10(@types/node@26.1.1)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.0 + expect-type: 1.4.0 magic-string: 0.30.21 - obug: 2.1.1 + obug: 2.1.3 pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.0.0 + picomatch: 4.0.5 + std-env: 4.2.0 tinybench: 2.9.0 - tinyexec: 1.1.1 - tinyglobby: 0.2.16 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@25.5.2)(esbuild@0.28.1) + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.5.2 + '@types/node': 26.1.1 transitivePeerDependencies: - msw @@ -3350,8 +3212,8 @@ snapshots: yocto-queue@0.1.0: {} - zod-to-json-schema@3.25.2(zod@4.3.6): + zod-to-json-schema@3.25.2(zod@4.4.3): dependencies: - zod: 4.3.6 + zod: 4.4.3 - zod@4.3.6: {} + zod@4.4.3: {} diff --git a/src/core/dashboard-client.ts b/src/core/dashboard-client.ts index a73ee56..199ac71 100644 --- a/src/core/dashboard-client.ts +++ b/src/core/dashboard-client.ts @@ -1,5 +1,5 @@ // AUTO-GENERATED by scripts/build-dashboard-client.mjs — do not edit by hand. -// Bundled status-dashboard client: Preact 10.29.2 + htm 3.1.1 + app. +// Bundled status-dashboard client: Preact 10.29.7 + htm 3.1.1 + app. // Source: src/dashboard-client/main.js. Regenerate with: pnpm run build:dashboard -export const DASHBOARD_CLIENT = "\"use strict\";(()=>{var J,b,we,et,N,ge,Ce,Me,se,W,L,Se,le,ae,oe,tt,O={},Y=[],nt=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Q=Array.isArray;function R(t,e){for(var n in e)t[n]=e[n];return t}function re(t){t&&t.parentNode&&t.parentNode.removeChild(t)}function ce(t,e,n){var s,i,a,l={};for(a in e)a==\"key\"?s=e[a]:a==\"ref\"?i=e[a]:l[a]=e[a];if(arguments.length>2&&(l.children=arguments.length>3?J.call(arguments,2):n),typeof t==\"function\"&&t.defaultProps!=null)for(a in t.defaultProps)l[a]===void 0&&(l[a]=t.defaultProps[a]);return K(t,l,s,i,null)}function K(t,e,n,s,i){var a={type:t,props:e,key:n,ref:s,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:i==null?++we:i,__i:-1,__u:0};return i==null&&b.vnode!=null&&b.vnode(a),a}function X(t){return t.children}function z(t,e){this.props=t,this.context=e}function D(t,e){if(e==null)return t.__?D(t.__,t.__i+1):null;for(var n;ee&&N.sort(Me),t=N.shift(),e=N.length,st(t)}finally{N.length=G.__r=0}}function Ee(t,e,n,s,i,a,l,u,p,c,r){var o,_,v,m,h,y,f,$=s&&s.__k||Y,g=e.length;for(p=at(n,e,$,p,g),o=0;o0?l=t.__k[a]=K(l.type,l.props,l.key,l.ref?l.ref:null,l.__v):t.__k[a]=l,p=a+_,l.__=t,l.__b=t.__b+1,u=null,(c=l.__i=ot(l,n,p,o))!=-1&&(o--,(u=n[c])&&(u.__u|=2)),u==null||u.__v==null?(c==-1&&(i>r?_--:ip?_--:_++,l.__u|=4))):t.__k[a]=null;if(o)for(a=0;a(r?1:0)){for(i=n-1,a=n+1;i>=0||a=0?i--:a++])!=null&&(2&c.__u)==0&&u==c.key&&p==c.type)return l}return-1}function be(t,e,n){e[0]==\"-\"?t.setProperty(e,n==null?\"\":n):t[e]=n==null?\"\":typeof n!=\"number\"||nt.test(e)?n:n+\"px\"}function V(t,e,n,s,i){var a,l;e:if(e==\"style\")if(typeof n==\"string\")t.style.cssText=n;else{if(typeof s==\"string\"&&(t.style.cssText=s=\"\"),s)for(e in s)n&&e in n||be(t.style,e,\"\");if(n)for(e in n)s&&n[e]==s[e]||be(t.style,e,n[e])}else if(e[0]==\"o\"&&e[1]==\"n\")a=e!=(e=e.replace(Se,\"$1\")),l=e.toLowerCase(),e=l in t||e==\"onFocusOut\"||e==\"onFocusIn\"?l.slice(2):e.slice(2),t.l||(t.l={}),t.l[e+a]=n,n?s?n[L]=s[L]:(n[L]=le,t.addEventListener(e,a?oe:ae,a)):t.removeEventListener(e,a?oe:ae,a);else{if(i==\"http://www.w3.org/2000/svg\")e=e.replace(/xlink(H|:h)/,\"h\").replace(/sName$/,\"s\");else if(e!=\"width\"&&e!=\"height\"&&e!=\"href\"&&e!=\"list\"&&e!=\"form\"&&e!=\"tabIndex\"&&e!=\"download\"&&e!=\"rowSpan\"&&e!=\"colSpan\"&&e!=\"role\"&&e!=\"popover\"&&e in t)try{t[e]=n==null?\"\":n;break e}catch{}typeof n==\"function\"||(n==null||n===!1&&e[4]!=\"-\"?t.removeAttribute(e):t.setAttribute(e,e==\"popover\"&&n==1?\"\":n))}}function xe(t){return function(e){if(this.l){var n=this.l[e.type+t];if(e[W]==null)e[W]=le++;else if(e[W]0?t:Q(t)?t.map(Te):t.constructor!==void 0?null:R({},t)}function it(t,e,n,s,i,a,l,u,p){var c,r,o,_,v,m,h,y=n.props||O,f=e.props,$=e.type;if($==\"svg\"?i=\"http://www.w3.org/2000/svg\":$==\"math\"?i=\"http://www.w3.org/1998/Math/MathML\":i||(i=\"http://www.w3.org/1999/xhtml\"),a!=null){for(c=0;c=n.__.length&&n.__.push({}),n.__[t]}function q(t){return U=1,rt(Ke,t)}function rt(t,e,n){var s=ve(I++,2);if(s.t=t,!s.__c&&(s.__=[n?n(e):Ke(void 0,e),function(u){var p=s.__N?s.__N[0]:s.__[0],c=s.t(p,u);p!==c&&(s.__N=[c,s.__[1]],s.__c.setState({}))}],s.__c=w,!w.__f)){var i=function(u,p,c){if(!s.__c.__H)return!0;var r=s.__c.__H.__.filter(function(_){return _.__c});if(r.every(function(_){return!_.__N}))return!a||a.call(this,u,p,c);var o=s.__c.props!==u;return r.some(function(_){if(_.__N){var v=_.__[0];_.__=_.__N,_.__N=void 0,v!==_.__[0]&&(o=!0)}}),a&&a.call(this,u,p,c)||o};w.__f=!0;var a=w.shouldComponentUpdate,l=w.componentWillUpdate;w.componentWillUpdate=function(u,p,c){if(this.__e){var r=a;a=void 0,i(u,p,c),a=r}l&&l.call(this,u,p,c)},w.shouldComponentUpdate=i}return s.__N||s.__}function ee(t,e){var n=ve(I++,3);!C.__s&&We(n.__H,e)&&(n.__=t,n.u=e,w.__H.__h.push(n))}function fe(t){return U=5,te(function(){return{current:t}},[])}function te(t,e){var n=ve(I++,7);return We(n.__H,e)&&(n.__=t(),n.__H=e,n.__h=t),n.__}function he(t,e){return U=8,te(function(){return t},e)}function ct(){for(var t;t=Ve.shift();){var e=t.__H;if(t.__P&&e)try{e.__h.some(Z),e.__h.some(_e),e.__h=[]}catch(n){e.__h=[],C.__e(n,t.__v)}}}C.__b=function(t){w=null,De&&De(t)},C.__=function(t,e){t&&e.__k&&e.__k.__m&&(t.__m=e.__k.__m),Fe&&Fe(t,e)},C.__r=function(t){je&&je(t),I=0;var e=(w=t.__c).__H;e&&(pe===w?(e.__h=[],w.__h=[],e.__.some(function(n){n.__N&&(n.__=n.__N),n.u=n.__N=void 0})):(e.__h.some(Z),e.__h.some(_e),e.__h=[],I=0)),pe=w},C.diffed=function(t){Le&&Le(t);var e=t.__c;e&&e.__H&&(e.__H.__h.length&&(Ve.push(e)!==1&&He===C.requestAnimationFrame||((He=C.requestAnimationFrame)||dt)(ct)),e.__H.__.some(function(n){n.u&&(n.__H=n.u),n.u=void 0})),pe=w=null},C.__c=function(t,e){e.some(function(n){try{n.__h.some(Z),n.__h=n.__h.filter(function(s){return!s.__||_e(s)})}catch(s){e.some(function(i){i.__h&&(i.__h=[])}),e=[],C.__e(s,n.__v)}}),Ie&&Ie(t,e)},C.unmount=function(t){Ue&&Ue(t);var e,n=t.__c;n&&n.__H&&(n.__H.__.some(function(s){try{Z(s)}catch(i){e=i}}),n.__H=void 0,e&&C.__e(e,n.__v))};var Be=typeof requestAnimationFrame==\"function\";function dt(t){var e,n=function(){clearTimeout(s),Be&&cancelAnimationFrame(e),setTimeout(t)},s=setTimeout(n,35);Be&&(e=requestAnimationFrame(n))}function Z(t){var e=w,n=t.__c;typeof n==\"function\"&&(t.__c=void 0,n()),w=e}function _e(t){var e=w;t.__c=t.__(),w=e}function We(t,e){return!t||t.length!==e.length||e.some(function(n,s){return n!==t[s]})}function Ke(t,e){return typeof e==\"function\"?e(t):e}var Oe=function(t,e,n,s){var i;e[0]=0;for(var a=1;a=5&&((l||!_&&a===5)&&(p.push(a,0,l,i),a=6),_&&(p.push(a,_,0,i),a=6)),l=\"\"},r=0;r\"?(a=1,l=\"\"):l=s+l[0]:u?s===u?u=\"\":l+=s:s==='\"'||s===\"'\"?u=s:s===\">\"?(c(),a=1):a&&(s===\"=\"?(a=5,i=l,l=\"\"):s===\"/\"&&(a<5||n[r][o+1]===\">\")?(c(),a===3&&(p=p[0]),a=p,(p=p[0]).push(2,0,a),a=0):s===\" \"||s===\"\t\"||s===`\n`||s===\"\\r\"?(c(),a=2):l+=s),a===3&&l===\"!--\"&&(a=4,p=p[0])}return c(),p})(t)),e),arguments,[])).length>1?e:e[0]}var d=Ye.bind(ce),Ge=window.__QRING__&&window.__QRING__.token||\"\",ye=Ge?\"?token=\"+encodeURIComponent(Ge):\"\",A=t=>''+t+\"\",k={health:A(''),environment:A(''),decay:A(''),superposition:A(''),entangle:A(''),tunnel:A(''),anomaly:A(''),audit:A(''),key:A(''),hook:A(''),approve:A(''),manifest:A(''),policy:A(''),memory:A(''),folder:'',search:'',lock:''};function me(t){if(!t)return\"env-default\";let e=t.toLowerCase();return e===\"prod\"||e===\"production\"?\"env-prod\":e===\"staging\"||e===\"stage\"?\"env-staging\":e===\"dev\"||e===\"development\"?\"env-dev\":e===\"test\"||e===\"testing\"?\"env-test\":\"env-default\"}function Qe(t,e){return e||t>=90?\"var(--danger)\":t>=75?\"var(--warning)\":\"var(--accent)\"}function Xe(t){let e=new Date(t);return String(e.getHours()).padStart(2,\"0\")+\":\"+String(e.getMinutes()).padStart(2,\"0\")+\":\"+String(e.getSeconds()).padStart(2,\"0\")}function Ze(t){if(!t)return\"\\u2014\";let e=Math.floor((Date.now()-new Date(t).getTime())/1e3);return isNaN(e)?\"\\u2014\":e<5?\"just now\":e<60?e+\"s ago\":e<3600?Math.floor(e/60)+\"m ago\":e<86400?Math.floor(e/3600)+\"h ago\":Math.floor(e/86400)+\"d ago\"}function $e(t){return t==null?\"\\u2014\":t<0?\"expired\":t<60?t+\"s\":t<3600?Math.floor(t/60)+\"m\":t<86400?Math.floor(t/3600)+\"h\":Math.floor(t/86400)+\"d\"}var j=({svg:t})=>d``;function M({icon:t,title:e,aside:n,asideStyle:s}){return d`
\n <${j} svg=${t} />${e}\n ${n!=null?d`${n}`:null}\n
`}function S({delay:t=0,wide:e=!1,style:n=\"\",children:s}){let i=\"card\"+(e?\" grid-wide\":\"\"),a=\"animation-delay:\"+t+\"ms\"+(n?\";\"+n:\"\");return d`
${s}
`}function E({children:t,cta:e}){return d`
${t}${e?d`${e}`:null}
`}function Je({snap:t,paused:e,connState:n,onPause:s,onRefresh:i,lastUpdate:a}){let l=e?\"paused\":n===\"live\"?\"live\":n===\"reconnecting\"?\"reconnecting\\u2026\":\"connecting\\u2026\",u=\"status-dot\"+(e?\" paused\":n===\"reconnecting\"?\" disconnected\":\"\");return d`\n
\n
\n

\n \n q-ring\n quantum status\n

\n \n \n ${\" v\"+(t?t.version:\"\")}\n \n \n <${j} svg=${k.folder} /> ${t?t.projectPath:\"\"}\n \n
\n
\n ${a}\n \n \n \n \n JSON\n \n ${l}\n
\n
\n `}function ut({snap:t}){let e=t,n=e.auditMetrics||{total:0,byAction:{},topRead:[]},s=n.byAction.read||0,i=(n.byAction.write||0)+(n.byAction.delete||0)+(n.byAction.rotate||0),a=n.byAction.policy_deny||0,l=(e.hooks||[]).filter(r=>r.enabled).length,u=(e.approvals||[]).filter(r=>r.valid).length,p=(e.anomalies||[]).length,c=[{label:\"Secrets\",icon:k.key,value:e.health.total,sub:e.health.healthy+\" healthy \\xB7 \"+e.health.stale+\" stale \\xB7 \"+e.health.expired+\" expired\",cls:\"\"},{label:\"Environment\",icon:k.environment,value:e.environment?e.environment.env:\"none\",sub:e.environment?\"via \"+e.environment.source:\"no env detected\",cls:e.environment?\"\":\"dim\"},{label:\"Protected\",icon:k.lock,value:e.protectedCount,sub:\"require approval\",cls:e.protectedCount>0?\"warning\":\"dim\"},{label:\"Approvals\",icon:k.approve,value:u,sub:\"active grants\",cls:u>0?\"green\":\"dim\"},{label:\"Hooks\",icon:k.hook,value:l,sub:\"enabled / \"+(e.hooks||[]).length+\" total\",cls:l>0?\"\":\"dim\"},{label:\"Reads (24h)\",icon:k.audit,value:s,sub:i+\" writes \\xB7 \"+a+\" denied\",cls:\"\"},{label:\"Anomalies\",icon:k.anomaly,value:p,sub:p?\"investigate now\":\"all clear\",cls:p?\"danger\":\"green\"}];return d`
\n ${c.map(r=>d`
\n
<${j} svg=${r.icon} />${r.label}
\n
${r.value}
\n
${r.sub}
\n
`)}\n
`}function pt({snap:t,delay:e}){let n=t.health,s=n.total||1,i=42,a=2*Math.PI*i,l=[{v:n.healthy,c:\"var(--accent)\"},{v:n.stale,c:\"var(--warning)\"},{v:n.expired,c:\"var(--danger)\"},{v:n.noDecay,c:\"var(--text-dim)\"}],u=0,p=l.map((r,o)=>{let _=r.v/s*a,v=d``;return u+=_,v}),c=t.scopes||{global:0,project:0,team:0,org:0};return d`<${S} delay=${e}>\n <${M} icon=${k.health} title=\"Health Summary\" aside=${n.healthy+\"/\"+n.total+\" healthy\"} />\n
\n
\n ${p}\n
${n.total}secrets
\n
\n
\n
Healthy ${n.healthy}
\n
Stale ${n.stale}
\n
Expired ${n.expired}
\n
No decay ${n.noDecay}
\n
\n
\n
\n ${[\"global\",\"project\",\"team\",\"org\"].filter(r=>c[r]).map(r=>d`${c[r]}${r}`)}\n
\n `}function _t({snap:t,delay:e}){let n=t.environment;return d`<${S} delay=${e}>\n <${M} icon=${k.environment} title=\"Environment\" />\n ${n?d`
${n.env}detected via ${n.source}
`:d`<${E} cta=\"$ qring env --project-path .\">No environment detected.`}\n `}function vt({snap:t,delay:e}){let n=t.manifest;if(!n)return d`<${S} delay=${e}>\n <${M} icon=${k.manifest} title=\"Manifest\" />\n <${E} cta=${d`$ qring wizard `}>No .q-ring.json manifest in this project.\n `;let s=n.required-n.missing.length-n.expired.length-n.stale.length,i=n.required?Math.max(0,Math.round(s/n.required*100)):100,a=n.missing.length?\"var(--danger)\":n.expired.length||n.stale.length?\"var(--warning)\":\"var(--green)\",l=(p,c,r)=>d`
${p} (${c.length})
${c.map(o=>d`${o}`)}
`,u=n.missing.length||n.expired.length||n.stale.length;return d`<${S} delay=${e}>\n <${M} icon=${k.manifest} title=\"Manifest\" aside=${n.required+\" required / \"+n.declared+\" declared\"} />\n
\n
healthy
${i}%
\n
\n
\n ${n.missing.length?l(\"Missing required\",n.missing,\"miss\"):null}\n ${n.expired.length?l(\"Expired\",n.expired,\"exp\"):null}\n ${n.stale.length?l(\"Stale\",n.stale,\"exp\"):null}\n ${u?null:d`
All required keys present and healthy.
`}\n
\n `}function ft({snap:t,delay:e}){let n=t.policy||{counts:{}},s=n.counts||{};if(!(n.hasMcpPolicy||n.hasExecPolicy||n.hasSecretPolicy))return d`<${S} delay=${e}>\n <${M} icon=${k.policy} title=\"Policy\" />\n <${E} cta=${d`add policy to .q-ring.json`}>No governance policy declared.\n `;let a=({label:u,value:p})=>d`
${u}${p||\"\\u2014\"}
`,l=[n.hasMcpPolicy?\"mcp\":\"\",n.hasExecPolicy?\"exec\":\"\",n.hasSecretPolicy?\"secrets\":\"\"].filter(Boolean).join(\" \\xB7 \")||\"none\";return d`<${S} delay=${e}>\n <${M} icon=${k.policy} title=\"Policy\" aside=${l} />\n
\n <${a} label=\"MCP allow tools\" value=${s.allowTools} />\n <${a} label=\"MCP deny tools\" value=${s.denyTools} />\n <${a} label=\"Denied keys\" value=${s.deniedKeys} />\n <${a} label=\"Denied tags\" value=${s.deniedTags} />\n <${a} label=\"Exec allow\" value=${s.allowCommands} />\n <${a} label=\"Exec deny\" value=${s.denyCommands} />\n <${a} label=\"Tag → approval\" value=${s.requireApprovalForTags} />\n <${a} label=\"Tag → rotation\" value=${s.requireRotationFormatForTags} />\n ${n.maxTtlSeconds?d`<${a} label=\"Max TTL\" value=${$e(n.maxTtlSeconds)} />`:null}\n ${n.maxRuntimeSeconds?d`<${a} label=\"Max exec runtime\" value=${$e(n.maxRuntimeSeconds)} />`:null}\n
\n `}function ht({snap:t,delay:e}){let n=(t.secrets||[]).filter(s=>s.decay&&s.decay.timeRemaining).sort((s,i)=>(s.decay.secondsRemaining||0)-(i.decay.secondsRemaining||0));return d`<${S} delay=${e}>\n <${M} icon=${k.decay} title=\"Decay Timers\" aside=${n.length+\" tracked\"} />\n ${n.length?d`
${n.slice(0,12).map(s=>{let i=Math.min(s.decay.lifetimePercent,100),a=Qe(i,s.decay.isExpired),l=s.decay.isExpired?\"expired\":s.decay.timeRemaining||\"\";return d`
\n ${s.key}\n
\n ${l}\n
`})}
`:d`<${E} cta=\"$ qring set KEY --ttl 86400\">No secrets with decay configured.`}\n `}function yt({snap:t,delay:e}){let n=(t.secrets||[]).filter(s=>s.type===\"superposition\"&&s.environments&&s.environments.length);return d`<${S} delay=${e}>\n <${M} icon=${k.superposition} title=\"Superposition\" aside=${n.length+\" multi-env\"} />\n ${n.length?d`
${n.slice(0,14).map(s=>d`
\n ${s.key}\n ${(s.environments||[]).map(i=>d`${i}${i===s.defaultEnv?\" \\u2713\":\"\"}`)}\n
`)}
`:d`<${E} cta=\"$ qring set KEY --env prod\">No secrets in superposition.`}\n `}function $t({snap:t,delay:e}){let n=t.entanglements||[],s=new Set,i=n.filter(h=>{let y=[h.source.service,h.source.key,h.target.service,h.target.key].sort().join(\"|\");return s.has(y)?!1:(s.add(y),!0)}),a=[],l=new Map;for(let h of i)for(let y of[h.source.key,h.target.key])l.has(y)||(l.set(y,a.length),a.push(y));let u=i.length>0&&a.length<=14,p=320,c=210,r=p/2,o=c/2,_=Math.min(p,c)/2-34,v=a.map((h,y)=>{let f=2*Math.PI*y/a.length-Math.PI/2;return{x:r+_*Math.cos(f),y:o+_*Math.sin(f)}}),m=h=>h.length>14?h.slice(0,12)+\"\\u2026\":h;return d`<${S} delay=${e}>\n <${M} icon=${k.entangle} title=\"Entanglement\" aside=${i.length+\" pair\"+(i.length===1?\"\":\"s\")+\" \\xB7 \"+a.length+\" keys\"} />\n ${i.length?u?d`\n ${i.map((h,y)=>{let f=v[l.get(h.source.key)],$=v[l.get(h.target.key)];return d``})}\n ${a.map((h,y)=>{let f=v[y],$=f.x>=r;return d`\n \n ${m(h)}${h}\n `})}\n `:d`
${i.slice(0,12).map((h,y)=>d`
\n ${h.source.key}${h.target.key}\n
`)}
`:d`<${E} cta=\"$ qring entangle KEY_A KEY_B\">No entangled secrets.`}\n `}function mt({snap:t,delay:e}){let n=t.tunnels||[];return d`<${S} delay=${e}>\n <${M} icon=${k.tunnel} title=\"Quantum Tunnels\" aside=${n.length+\" live\"} />\n ${n.length?d`
${n.map(s=>{let i=s.expiresAt?Math.max(0,Math.floor((s.expiresAt-Date.now())/1e3)):null;return d`
${s.id}
\n reads: ${s.accessCount}${s.maxReads?\"/\"+s.maxReads:\"\"}\n ${i!==null?d`expires: ${i}s`:d`no expiry`}\n
`})}
`:d`<${E} cta=${d`$ qring tunnel create `}>No active tunnels.`}\n `}function gt({snap:t,delay:e}){let n=t.approvals||[];return d`<${S} delay=${e}>\n <${M} icon=${k.approve} title=\"Approvals\" aside=${n.filter(s=>s.valid).length+\" valid\"} />\n ${n.length?d`
${n.slice(0,8).map(s=>{let i=s.tampered?\"tampered\":s.secondsRemaining<300?\"expiring\":\"\";return d`
\n
${s.key}${s.tampered?\" \\u26A0 TAMPERED\":\"\"}${$e(s.secondsRemaining)} left
\n
${s.reason||\"(no reason)\"}
\n
scope:${s.scope}by:${s.grantedBy}
\n
`})}
`:d`<${E} cta=${'$ qring approve KEY --for 1800 --reason \"...\"'}>No active approvals.`}\n `}function kt({snap:t,delay:e}){let n=t.hooks||[];return d`<${S} delay=${e}>\n <${M} icon=${k.hook} title=\"Hooks\" aside=${n.filter(s=>s.enabled).length+\" enabled / \"+n.length} />\n ${n.length?d`
${n.slice(0,8).map(s=>d`
\n ${s.type}\n ${s.description||s.matchSummary}\n ${s.id}\n
`)}
`:d`<${E} cta=${'$ qring hook add --tag payments --action rotate --exec \"...\"'}>No hooks registered.`}\n `}function bt({snap:t,delay:e}){return d`<${S} delay=${e}>\n <${M} icon=${k.memory} title=\"Agent Memory\" />\n
\n ${t.memoryKeys||0}\n memory key${t.memoryKeys===1?\"\":\"s\"} encrypted at ~/.config/q-ring/agent-memory.enc\n
\n `}function xt({snap:t,delay:e}){let n=t.anomalies||[];if(!n.length)return d`<${S} delay=${e} wide=${!0}>\n <${M} icon=${k.anomaly} title=\"Anomaly Alerts\" aside=\"all clear\" asideStyle=\"color:var(--green)\" />\n <${E}>No anomalies detected — quantum field is stable.\n `;let s=i=>i===\"tampered\"?\"$ qring audit:verify\":i===\"burst\"?\"$ qring audit --key \\u2026 --action read\":i===\"unusual-hour\"?'$ qring audit --action read --since \"1am\"':\"$ qring audit --limit 100\";return d`<${S} delay=${e} wide=${!0}>\n <${M} icon=${k.anomaly} title=\"Anomaly Alerts\" aside=${n.length+\" active\"} asideStyle=\"color:var(--danger)\" />\n
${n.map((i,a)=>d`
\n
${i.type}
\n
${i.description}
\n
${s(i.type)}
\n
`)}
\n `}function wt(t,e){let n=e.trim().toLowerCase();return n?n===\"expired\"?t.filter(s=>s.decay&&s.decay.isExpired):n===\"stale\"?t.filter(s=>s.decay&&s.decay.isStale&&!s.decay.isExpired):n===\"protected\"?t.filter(s=>s.requiresApproval):t.filter(s=>[s.key,s.scope,s.type,s.provider||\"\",s.defaultEnv||\"\",(s.environments||[]).join(\" \"),(s.tags||[]).join(\" \")].join(\" \").toLowerCase().indexOf(n)!==-1):t.slice()}function Ct(t,e){let n=e.dir===\"asc\"?1:-1;return t.sort((s,i)=>{let a,l;switch(e.col){case\"key\":a=s.key,l=i.key;break;case\"scope\":a=s.scope,l=i.scope;break;case\"env\":a=s.defaultEnv||(s.environments||[])[0]||\"\",l=i.defaultEnv||(i.environments||[])[0]||\"\";break;case\"type\":a=s.type,l=i.type;break;case\"decay\":a=s.decay&&s.decay.secondsRemaining!=null?s.decay.secondsRemaining:1/0,l=i.decay&&i.decay.secondsRemaining!=null?i.decay.secondsRemaining:1/0;break;case\"tags\":a=(s.tags||[]).join(\",\"),l=(i.tags||[]).join(\",\");break;default:a=s.lastAccessedAt?new Date(s.lastAccessedAt).getTime():0,l=i.lastAccessedAt?new Date(i.lastAccessedAt).getTime():0}return al?1*n:0})}function Mt({snap:t,query:e,setQuery:n,sort:s,setSort:i,searchRef:a}){let l=t.secrets||[],u=te(()=>Ct(wt(l,e),s),[l,e,s]),p=[{id:\"key\",label:\"Key\"},{id:\"scope\",label:\"Scope\"},{id:\"env\",label:\"Env\"},{id:\"type\",label:\"Type\"},{id:\"decay\",label:\"Decay\"},{id:\"tags\",label:\"Tags\"},{id:\"lastAccessedAt\",label:\"Last read\"}],c=r=>{s.col===r?i({col:r,dir:s.dir===\"asc\"?\"desc\":\"asc\"}):i({col:r,dir:r===\"key\"?\"asc\":\"desc\"})};return d`
<${S} wide=${!0} style=\"margin-bottom:16px\">\n <${M} icon=${k.key} title=\"Secrets\" aside=${u.length+\" of \"+l.length} />\n
\n \n \n \n \n \n
\n
\n ${u.length?d`\n ${p.map(r=>d``)}\n ${u.map(r=>{let o=r.type===\"superposition\"?(r.environments||[]).map(m=>d`${m}${m===r.defaultEnv?\" \\u2713\":\"\"}`):d``,_=r.decay&&r.decay.timeRemaining?(()=>{let m=Math.min(r.decay.lifetimePercent,100),h=Qe(m,r.decay.isExpired);return d`${r.decay.isExpired?\"expired\":r.decay.timeRemaining}`})():d``,v=r.tags||[];return d`\n \n \n \n \n \n \n \n `})}\n
c(r.id)}>${r.label}${s.dir===\"asc\"?\"\\u25B2\":\"\\u25BC\"}
${r.key}${r.requiresApproval?d`<${j} svg=${k.lock} />`:null}${r.scope}${o}${r.type}${_}${v.slice(0,3).map(m=>d`${m}`)}${v.length>3?\"+\"+(v.length-3):\"\"}${Ze(r.lastAccessedAt)}
`:d`
No secrets match the filter.
`}\n
\n
`}function St({snap:t,delay:e,filter:n,setFilter:s}){let i=t.audit||[],a=t.auditMetrics||{byAction:{},bySource:{}},l=i.filter(o=>{if(n.action&&o.action!==n.action||n.source&&o.source!==n.source)return!1;let _=n.text.trim().toLowerCase();return!(_&&[o.key||\"\",o.action,o.source,o.detail||\"\",o.scope||\"\",o.env||\"\"].join(\" \").toLowerCase().indexOf(_)===-1)}),u=[\"read\",\"write\",\"delete\",\"rotate\",\"generate\",\"entangle\",\"tunnel\",\"teleport\",\"approve\",\"revoke\",\"policy_deny\",\"collapse\",\"export\"],p=[\"cli\",\"mcp\",\"agent\",\"ci\",\"hook\",\"api\"],c=(o,_)=>s({...n,[o]:n[o]===_?\"\":_}),r=t.auditChain;return d`<${S} delay=${e} wide=${!0}>\n <${M} icon=${k.audit} title=\"Audit Log (24h)\" aside=${l.length+\" of \"+i.length+\" shown \\xB7 window \"+(a.windowSeconds||86400)/3600+\"h\"} />\n ${r&&r.totalEvents>0?d`
\n ${r.intact?d`⛓ chain intact · ${r.totalEvents} events verified`:d`⛓ chain BROKEN at event #${r.brokenAt} · ${r.validEvents}/${r.totalEvents} valid — run $ qring audit:verify`}\n
`:null}\n
\n ${u.filter(o=>a.byAction[o]).length?u.filter(o=>a.byAction[o]).map(o=>d`c(\"action\",o)}>${o}${a.byAction[o]}`):d`no actions`}\n
\n ${p.filter(o=>a.bySource[o]).length?d`
${p.filter(o=>a.bySource[o]).map(o=>d`c(\"source\",o)}>${o}${a.bySource[o]}`)}
`:null}\n
\n \n \n
\n ${l.length?d`
${l.slice(0,80).map((o,_)=>d`
\n ${Xe(o.timestamp)}\n ${o.action}\n ${o.source}\n ${o.key||\"\\u2014\"} ${o.detail||\"\"}\n
`)}
`:d`<${E}>No audit events match the filter.`}\n `}function At(){let[t,e]=q(null),[n,s]=q(!1),[i,a]=q(\"connecting\"),[l,u]=q(\"\"),[p,c]=q({col:\"lastAccessedAt\",dir:\"desc\"}),[r,o]=q({action:\"\",source:\"\",text:\"\"}),[,_]=q(0),v=fe(n);v.current=n;let m=fe(null),h=he(g=>{try{e(typeof g==\"string\"?JSON.parse(g):g)}catch{}},[]),y=he(()=>{fetch(\"/api/status\"+ye,{cache:\"no-store\"}).then(g=>g.json()).then(h).catch(()=>{})},[h]);ee(()=>{let g=new EventSource(\"/events\"+ye);return g.onopen=()=>a(\"live\"),g.onmessage=x=>{v.current||h(x.data)},g.onerror=()=>a(\"reconnecting\"),()=>g.close()},[h]),ee(()=>{let g=setInterval(()=>_(x=>x+1),5e3);return()=>clearInterval(g)},[]),ee(()=>{let g=x=>{let T=x.target&&x.target.tagName;if(T===\"INPUT\"||T===\"TEXTAREA\"){x.key===\"Escape\"&&x.target.blur();return}x.key===\"/\"?(x.preventDefault(),m.current&&m.current.focus()):x.key===\"p\"||x.key===\"P\"?s(F=>!F):(x.key===\"r\"||x.key===\"R\")&&y()};return document.addEventListener(\"keydown\",g),()=>document.removeEventListener(\"keydown\",g)},[y]);let f=t?\"updated \"+Ze(t.timestamp):\"\\u2014\";if(!t)return d`
\n <${Je} snap=${null} paused=${n} connState=${i} onPause=${()=>s(g=>!g)} onRefresh=${y} lastUpdate=${f} />\n

Connecting to the live quantum stream…

\n
`;let $=t.auditMetrics||{total:0};return d`
\n <${Je} snap=${t} paused=${n} connState=${i} onPause=${()=>s(g=>!g)} onRefresh=${y} lastUpdate=${f} />\n <${ut} snap=${t} />\n
\n <${pt} snap=${t} delay=${0} />\n <${_t} snap=${t} delay=${60} />\n <${vt} snap=${t} delay=${120} />\n <${ft} snap=${t} delay=${180} />\n
\n <${Mt} snap=${t} query=${l} setQuery=${u} sort=${p} setSort=${c} searchRef=${m} />\n
\n <${ht} snap=${t} delay=${0} />\n <${yt} snap=${t} delay=${60} />\n <${$t} snap=${t} delay=${120} />\n <${mt} snap=${t} delay=${180} />\n
\n
\n <${gt} snap=${t} delay=${0} />\n <${kt} snap=${t} delay=${60} />\n <${bt} snap=${t} delay=${120} />\n
\n
\n <${xt} snap=${t} delay=${0} />\n <${St} snap=${t} delay=${60} filter=${r} setFilter=${o} />\n
\n

\n ${\"q-ring v\"+t.version+\" \\xB7 snapshot \"+Xe(t.timestamp)+\" \\xB7 \"+($.total||0)+\" audit events in last 24h\"}\n · keyboard: / search · P pause · R refresh\n

\n
`}qe(d`<${At} />`,document.getElementById(\"app\"));})();"; +export const DASHBOARD_CLIENT = "\"use strict\";(()=>{var J,b,we,tt,q,ge,Ce,Me,se,W,I,Se,ie,ae,oe,nt,O={},Y=[],st=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Q=Array.isArray;function T(t,e){for(var n in e)t[n]=e[n];return t}function le(t){t&&t.parentNode&&t.parentNode.removeChild(t)}function re(t,e,n){var s,o,a,l={};for(a in e)a==\"key\"?s=e[a]:a==\"ref\"?o=e[a]:l[a]=e[a];if(arguments.length>2&&(l.children=arguments.length>3?J.call(arguments,2):n),typeof t==\"function\"&&t.defaultProps!=null)for(a in t.defaultProps)l[a]===void 0&&(l[a]=t.defaultProps[a]);return K(t,l,s,o,null)}function K(t,e,n,s,o){var a={type:t,props:e,key:n,ref:s,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o==null?++we:o,__i:-1,__u:0};return o==null&&b.vnode!=null&&b.vnode(a),a}function X(t){return t.children}function z(t,e){this.props=t,this.context=e}function j(t,e){if(e==null)return t.__?j(t.__,t.__i+1):null;for(var n;ee&&q.sort(Me),t=q.shift(),e=q.length,at(t)}finally{q.length=G.__r=0}}function Ee(t,e,n,s,o,a,l,u,_,c,r){var p,i,v,f,h,y,m,$=s&&s.__k||Y,g=e.length;for(_=ot(n,e,$,_,g),p=0;p0?l=t.__k[a]=K(l.type,l.props,l.key,l.ref?l.ref:null,l.__v):t.__k[a]=l,_=a+i,l.__=t,l.__b=t.__b+1,u=null,(c=l.__i=it(l,n,_,p))!=-1&&(p--,(u=n[c])&&(u.__u|=2)),u==null||u.__v==null?(c==-1&&(o>r?i--:o_?i--:i++,l.__u|=4))):t.__k[a]=null;if(p)for(a=0;a(r?1:0)){for(o=n-1,a=n+1;o>=0||a=0?o--:a++])!=null&&(2&c.__u)==0&&u==c.key&&_==c.type)return l}return-1}function be(t,e,n){e[0]==\"-\"?t.setProperty(e,n==null?\"\":n):t[e]=n==null?\"\":typeof n!=\"number\"||st.test(e)?n:n+\"px\"}function V(t,e,n,s,o){var a,l;e:if(e==\"style\")if(typeof n==\"string\")t.style.cssText=n;else{if(typeof s==\"string\"&&(t.style.cssText=s=\"\"),s)for(e in s)n&&e in n||be(t.style,e,\"\");if(n)for(e in n)s&&n[e]==s[e]||be(t.style,e,n[e])}else if(e[0]==\"o\"&&e[1]==\"n\")a=e!=(e=e.replace(Se,\"$1\")),l=e.toLowerCase(),e=l in t||e==\"onFocusOut\"||e==\"onFocusIn\"?l.slice(2):e.slice(2),t.l||(t.l={}),t.l[e+a]=n,n?s?n[I]=s[I]:(n[I]=ie,t.addEventListener(e,a?oe:ae,a)):t.removeEventListener(e,a?oe:ae,a);else{if(o==\"http://www.w3.org/2000/svg\")e=e.replace(/xlink(H|:h)/,\"h\").replace(/sName$/,\"s\");else if(e!=\"width\"&&e!=\"height\"&&e!=\"href\"&&e!=\"list\"&&e!=\"form\"&&e!=\"tabIndex\"&&e!=\"download\"&&e!=\"rowSpan\"&&e!=\"colSpan\"&&e!=\"role\"&&e!=\"popover\"&&e in t)try{t[e]=n==null?\"\":n;break e}catch{}typeof n==\"function\"||(n==null||n===!1&&e[4]!=\"-\"?t.removeAttribute(e):t.setAttribute(e,e==\"popover\"&&n==1?\"\":n))}}function xe(t){return function(e){if(this.l){var n=this.l[e.type+t];if(e[W]==null)e[W]=ie++;else if(e[W]0?t:Q(t)?t.map(qe):t.constructor!==void 0?null:T({},t)}function lt(t,e,n,s,o,a,l,u,_){var c,r,p,i,v,f,h,y=n.props||O,m=e.props,$=e.type;if($==\"svg\"?o=\"http://www.w3.org/2000/svg\":$==\"math\"?o=\"http://www.w3.org/1998/Math/MathML\":o||(o=\"http://www.w3.org/1999/xhtml\"),a!=null){for(c=0;c=n.__.length&&n.__.push({}),n.__[t]}function N(t){return F=1,ct(ze,t)}function ct(t,e,n){var s=_e(U++,2);if(s.t=t,!s.__c&&(s.__=[n?n(e):ze(void 0,e),function(u){var _=s.__N?s.__N[0]:s.__[0],c=s.t(_,u);_!==c&&(s.__N=[c,s.__[1]],s.__c.setState({}))}],s.__c=x,!x.__f)){var o=function(u,_,c){if(!s.__c.__H)return!0;var r=!1,p=s.__c.props!==u;if(s.__c.__H.__.some(function(v){if(v.__N){r=!0;var f=v.__[0];v.__=v.__N,v.__N=void 0,f!==v.__[0]&&(p=!0)}}),a){var i=a.call(this,u,_,c);return r?i||p:i}return!r||p};x.__f=!0;var a=x.shouldComponentUpdate,l=x.componentWillUpdate;x.componentWillUpdate=function(u,_,c){if(this.__e){var r=a;a=void 0,o(u,_,c),a=r}l&&l.call(this,u,_,c)},x.shouldComponentUpdate=o}return s.__N||s.__}function ee(t,e){var n=_e(U++,3);!C.__s&&Ke(n.__H,e)&&(n.__=t,n.u=e,x.__H.__h.push(n))}function ve(t){return F=5,te(function(){return{current:t}},[])}function te(t,e){var n=_e(U++,7);return Ke(n.__H,e)&&(n.__=t(),n.__H=e,n.__h=t),n.__}function he(t,e){return F=8,te(function(){return t},e)}function dt(){for(var t;t=We.shift();){var e=t.__H;if(t.__P&&e)try{e.__h.some(Z),e.__h.some(pe),e.__h=[]}catch(n){e.__h=[],C.__e(n,t.__v)}}}C.__b=function(t){x=null,je&&je(t)},C.__=function(t,e){t&&e.__k&&e.__k.__m&&(t.__m=e.__k.__m),Be&&Be(t,e)},C.__r=function(t){Le&&Le(t),U=0;var e=(x=t.__c).__H;e&&(ue===x?(e.__h=[],x.__h=[],e.__.some(function(n){n.__N&&(n.__=n.__N),n.u=n.__N=void 0})):(e.__h.some(Z),e.__h.some(pe),e.__h=[],U=0)),ue=x},C.diffed=function(t){Ie&&Ie(t);var e=t.__c;e&&e.__H&&(e.__H.__h.length&&(We.push(e)!==1&&De===C.requestAnimationFrame||((De=C.requestAnimationFrame)||ut)(dt)),e.__H.__.some(function(n){n.u&&(n.__H=n.u,n.u=void 0)})),ue=x=null},C.__c=function(t,e){e.some(function(n){try{n.__h.some(Z),n.__h=n.__h.filter(function(s){return!s.__||pe(s)})}catch(s){e.some(function(o){o.__h&&(o.__h=[])}),e=[],C.__e(s,n.__v)}}),Ue&&Ue(t,e)},C.unmount=function(t){Fe&&Fe(t);var e,n=t.__c;n&&n.__H&&(n.__H.__.some(function(s){try{Z(s)}catch(o){e=o}}),n.__H=void 0,e&&C.__e(e,n.__v))};var Ve=typeof requestAnimationFrame==\"function\";function ut(t){var e,n=function(){clearTimeout(s),Ve&&cancelAnimationFrame(e),setTimeout(t)},s=setTimeout(n,35);Ve&&(e=requestAnimationFrame(n))}function Z(t){var e=x,n=t.__c;typeof n==\"function\"&&(t.__c=void 0,n()),x=e}function pe(t){var e=x;t.__c=t.__(),x=e}function Ke(t,e){return!t||t.length!==e.length||e.some(function(n,s){return n!==t[s]})}function ze(t,e){return typeof e==\"function\"?e(t):e}var Ye=function(t,e,n,s){var o;e[0]=0;for(var a=1;a=5&&((l||!i&&a===5)&&(_.push(a,0,l,o),a=6),i&&(_.push(a,i,0,o),a=6)),l=\"\"},r=0;r\"?(a=1,l=\"\"):l=s+l[0]:u?s===u?u=\"\":l+=s:s==='\"'||s===\"'\"?u=s:s===\">\"?(c(),a=1):a&&(s===\"=\"?(a=5,o=l,l=\"\"):s===\"/\"&&(a<5||n[r][p+1]===\">\")?(c(),a===3&&(_=_[0]),a=_,(_=_[0]).push(2,0,a),a=0):s===\" \"||s===\"\t\"||s===`\n`||s===\"\\r\"?(c(),a=2):l+=s),a===3&&l===\"!--\"&&(a=4,_=_[0])}return c(),_})(t)),e),arguments,[])).length>1?e:e[0]}var d=Ge.bind(re),Je=window.__QRING__&&window.__QRING__.token||\"\",fe=Je?\"?token=\"+encodeURIComponent(Je):\"\",A=t=>''+t+\"\",k={health:A(''),environment:A(''),decay:A(''),superposition:A(''),entangle:A(''),tunnel:A(''),anomaly:A(''),audit:A(''),key:A(''),hook:A(''),approve:A(''),manifest:A(''),policy:A(''),memory:A(''),folder:'',search:'',lock:''};function $e(t){if(!t)return\"env-default\";let e=t.toLowerCase();return e===\"prod\"||e===\"production\"?\"env-prod\":e===\"staging\"||e===\"stage\"?\"env-staging\":e===\"dev\"||e===\"development\"?\"env-dev\":e===\"test\"||e===\"testing\"?\"env-test\":\"env-default\"}function Xe(t,e){return e||t>=90?\"var(--danger)\":t>=75?\"var(--warning)\":\"var(--accent)\"}function Ze(t){let e=new Date(t);return String(e.getHours()).padStart(2,\"0\")+\":\"+String(e.getMinutes()).padStart(2,\"0\")+\":\"+String(e.getSeconds()).padStart(2,\"0\")}function et(t){if(!t)return\"\\u2014\";let e=Math.floor((Date.now()-new Date(t).getTime())/1e3);return isNaN(e)?\"\\u2014\":e<5?\"just now\":e<60?e+\"s ago\":e<3600?Math.floor(e/60)+\"m ago\":e<86400?Math.floor(e/3600)+\"h ago\":Math.floor(e/86400)+\"d ago\"}function ye(t){return t==null?\"\\u2014\":t<0?\"expired\":t<60?t+\"s\":t<3600?Math.floor(t/60)+\"m\":t<86400?Math.floor(t/3600)+\"h\":Math.floor(t/86400)+\"d\"}var L=({svg:t})=>d``;function M({icon:t,title:e,aside:n,asideStyle:s}){return d`
\n <${L} svg=${t} />${e}\n ${n!=null?d`${n}`:null}\n
`}function S({delay:t=0,wide:e=!1,style:n=\"\",children:s}){let o=\"card\"+(e?\" grid-wide\":\"\"),a=\"animation-delay:\"+t+\"ms\"+(n?\";\"+n:\"\");return d`
${s}
`}function E({children:t,cta:e}){return d`
${t}${e?d`${e}`:null}
`}function Qe({snap:t,paused:e,connState:n,onPause:s,onRefresh:o,lastUpdate:a}){let l=e?\"paused\":n===\"live\"?\"live\":n===\"reconnecting\"?\"reconnecting\\u2026\":\"connecting\\u2026\",u=\"status-dot\"+(e?\" paused\":n===\"reconnecting\"?\" disconnected\":\"\");return d`\n
\n
\n

\n \n q-ring\n quantum status\n

\n \n \n ${\" v\"+(t?t.version:\"\")}\n \n \n <${L} svg=${k.folder} /> ${t?t.projectPath:\"\"}\n \n
\n
\n ${a}\n \n \n \n \n JSON\n \n ${l}\n
\n
\n `}function pt({snap:t}){let e=t,n=e.auditMetrics||{total:0,byAction:{},topRead:[]},s=n.byAction.read||0,o=(n.byAction.write||0)+(n.byAction.delete||0)+(n.byAction.rotate||0),a=n.byAction.policy_deny||0,l=(e.hooks||[]).filter(r=>r.enabled).length,u=(e.approvals||[]).filter(r=>r.valid).length,_=(e.anomalies||[]).length,c=[{label:\"Secrets\",icon:k.key,value:e.health.total,sub:e.health.healthy+\" healthy \\xB7 \"+e.health.stale+\" stale \\xB7 \"+e.health.expired+\" expired\",cls:\"\"},{label:\"Environment\",icon:k.environment,value:e.environment?e.environment.env:\"none\",sub:e.environment?\"via \"+e.environment.source:\"no env detected\",cls:e.environment?\"\":\"dim\"},{label:\"Protected\",icon:k.lock,value:e.protectedCount,sub:\"require approval\",cls:e.protectedCount>0?\"warning\":\"dim\"},{label:\"Approvals\",icon:k.approve,value:u,sub:\"active grants\",cls:u>0?\"green\":\"dim\"},{label:\"Hooks\",icon:k.hook,value:l,sub:\"enabled / \"+(e.hooks||[]).length+\" total\",cls:l>0?\"\":\"dim\"},{label:\"Reads (24h)\",icon:k.audit,value:s,sub:o+\" writes \\xB7 \"+a+\" denied\",cls:\"\"},{label:\"Anomalies\",icon:k.anomaly,value:_,sub:_?\"investigate now\":\"all clear\",cls:_?\"danger\":\"green\"}];return d`
\n ${c.map(r=>d`
\n
<${L} svg=${r.icon} />${r.label}
\n
${r.value}
\n
${r.sub}
\n
`)}\n
`}function _t({snap:t,delay:e}){let n=t.health,s=n.total||1,o=42,a=2*Math.PI*o,l=[{v:n.healthy,c:\"var(--accent)\"},{v:n.stale,c:\"var(--warning)\"},{v:n.expired,c:\"var(--danger)\"},{v:n.noDecay,c:\"var(--text-dim)\"}],u=0,_=l.map((r,p)=>{let i=r.v/s*a,v=d``;return u+=i,v}),c=t.scopes||{global:0,project:0,team:0,org:0};return d`<${S} delay=${e}>\n <${M} icon=${k.health} title=\"Health Summary\" aside=${n.healthy+\"/\"+n.total+\" healthy\"} />\n
\n
\n ${_}\n
${n.total}secrets
\n
\n
\n
Healthy ${n.healthy}
\n
Stale ${n.stale}
\n
Expired ${n.expired}
\n
No decay ${n.noDecay}
\n
\n
\n
\n ${[\"global\",\"project\",\"team\",\"org\"].filter(r=>c[r]).map(r=>d`${c[r]}${r}`)}\n
\n `}function vt({snap:t,delay:e}){let n=t.environment;return d`<${S} delay=${e}>\n <${M} icon=${k.environment} title=\"Environment\" />\n ${n?d`
${n.env}detected via ${n.source}
`:d`<${E} cta=\"$ qring env --project-path .\">No environment detected.`}\n `}function ht({snap:t,delay:e}){let n=t.manifest;if(!n)return d`<${S} delay=${e}>\n <${M} icon=${k.manifest} title=\"Manifest\" />\n <${E} cta=${d`$ qring wizard `}>No .q-ring.json manifest in this project.\n `;let s=n.required-n.missing.length-n.expired.length-n.stale.length,o=n.required?Math.max(0,Math.round(s/n.required*100)):100,a=n.missing.length?\"var(--danger)\":n.expired.length||n.stale.length?\"var(--warning)\":\"var(--green)\",l=(_,c,r)=>d`
${_} (${c.length})
${c.map(p=>d`${p}`)}
`,u=n.missing.length||n.expired.length||n.stale.length;return d`<${S} delay=${e}>\n <${M} icon=${k.manifest} title=\"Manifest\" aside=${n.required+\" required / \"+n.declared+\" declared\"} />\n
\n
healthy
${o}%
\n
\n
\n ${n.missing.length?l(\"Missing required\",n.missing,\"miss\"):null}\n ${n.expired.length?l(\"Expired\",n.expired,\"exp\"):null}\n ${n.stale.length?l(\"Stale\",n.stale,\"exp\"):null}\n ${u?null:d`
All required keys present and healthy.
`}\n
\n `}function ft({snap:t,delay:e}){let n=t.policy||{counts:{}},s=n.counts||{};if(!(n.hasMcpPolicy||n.hasExecPolicy||n.hasSecretPolicy))return d`<${S} delay=${e}>\n <${M} icon=${k.policy} title=\"Policy\" />\n <${E} cta=${d`add policy to .q-ring.json`}>No governance policy declared.\n `;let a=({label:u,value:_})=>d`
${u}${_||\"\\u2014\"}
`,l=[n.hasMcpPolicy?\"mcp\":\"\",n.hasExecPolicy?\"exec\":\"\",n.hasSecretPolicy?\"secrets\":\"\"].filter(Boolean).join(\" \\xB7 \")||\"none\";return d`<${S} delay=${e}>\n <${M} icon=${k.policy} title=\"Policy\" aside=${l} />\n
\n <${a} label=\"MCP allow tools\" value=${s.allowTools} />\n <${a} label=\"MCP deny tools\" value=${s.denyTools} />\n <${a} label=\"Denied keys\" value=${s.deniedKeys} />\n <${a} label=\"Denied tags\" value=${s.deniedTags} />\n <${a} label=\"Exec allow\" value=${s.allowCommands} />\n <${a} label=\"Exec deny\" value=${s.denyCommands} />\n <${a} label=\"Tag → approval\" value=${s.requireApprovalForTags} />\n <${a} label=\"Tag → rotation\" value=${s.requireRotationFormatForTags} />\n ${n.maxTtlSeconds?d`<${a} label=\"Max TTL\" value=${ye(n.maxTtlSeconds)} />`:null}\n ${n.maxRuntimeSeconds?d`<${a} label=\"Max exec runtime\" value=${ye(n.maxRuntimeSeconds)} />`:null}\n
\n `}function yt({snap:t,delay:e}){let n=(t.secrets||[]).filter(s=>s.decay&&s.decay.timeRemaining).sort((s,o)=>(s.decay.secondsRemaining||0)-(o.decay.secondsRemaining||0));return d`<${S} delay=${e}>\n <${M} icon=${k.decay} title=\"Decay Timers\" aside=${n.length+\" tracked\"} />\n ${n.length?d`
${n.slice(0,12).map(s=>{let o=Math.min(s.decay.lifetimePercent,100),a=Xe(o,s.decay.isExpired),l=s.decay.isExpired?\"expired\":s.decay.timeRemaining||\"\";return d`
\n ${s.key}\n
\n ${l}\n
`})}
`:d`<${E} cta=\"$ qring set KEY --ttl 86400\">No secrets with decay configured.`}\n `}function $t({snap:t,delay:e}){let n=(t.secrets||[]).filter(s=>s.type===\"superposition\"&&s.environments&&s.environments.length);return d`<${S} delay=${e}>\n <${M} icon=${k.superposition} title=\"Superposition\" aside=${n.length+\" multi-env\"} />\n ${n.length?d`
${n.slice(0,14).map(s=>d`
\n ${s.key}\n ${(s.environments||[]).map(o=>d`${o}${o===s.defaultEnv?\" \\u2713\":\"\"}`)}\n
`)}
`:d`<${E} cta=\"$ qring set KEY --env prod\">No secrets in superposition.`}\n `}function mt({snap:t,delay:e}){let n=t.entanglements||[],s=new Set,o=n.filter(h=>{let y=[h.source.service,h.source.key,h.target.service,h.target.key].sort().join(\"|\");return s.has(y)?!1:(s.add(y),!0)}),a=[],l=new Map;for(let h of o)for(let y of[h.source.key,h.target.key])l.has(y)||(l.set(y,a.length),a.push(y));let u=o.length>0&&a.length<=14,_=320,c=210,r=_/2,p=c/2,i=Math.min(_,c)/2-34,v=a.map((h,y)=>{let m=2*Math.PI*y/a.length-Math.PI/2;return{x:r+i*Math.cos(m),y:p+i*Math.sin(m)}}),f=h=>h.length>14?h.slice(0,12)+\"\\u2026\":h;return d`<${S} delay=${e}>\n <${M} icon=${k.entangle} title=\"Entanglement\" aside=${o.length+\" pair\"+(o.length===1?\"\":\"s\")+\" \\xB7 \"+a.length+\" keys\"} />\n ${o.length?u?d`\n ${o.map((h,y)=>{let m=v[l.get(h.source.key)],$=v[l.get(h.target.key)];return d``})}\n ${a.map((h,y)=>{let m=v[y],$=m.x>=r;return d`\n \n ${f(h)}${h}\n `})}\n `:d`
${o.slice(0,12).map((h,y)=>d`
\n ${h.source.key}${h.target.key}\n
`)}
`:d`<${E} cta=\"$ qring entangle KEY_A KEY_B\">No entangled secrets.`}\n `}function gt({snap:t,delay:e}){let n=t.tunnels||[];return d`<${S} delay=${e}>\n <${M} icon=${k.tunnel} title=\"Quantum Tunnels\" aside=${n.length+\" live\"} />\n ${n.length?d`
${n.map(s=>{let o=s.expiresAt?Math.max(0,Math.floor((s.expiresAt-Date.now())/1e3)):null;return d`
${s.id}
\n reads: ${s.accessCount}${s.maxReads?\"/\"+s.maxReads:\"\"}\n ${o!==null?d`expires: ${o}s`:d`no expiry`}\n
`})}
`:d`<${E} cta=${d`$ qring tunnel create `}>No active tunnels.`}\n `}function kt({snap:t,delay:e}){let n=t.approvals||[];return d`<${S} delay=${e}>\n <${M} icon=${k.approve} title=\"Approvals\" aside=${n.filter(s=>s.valid).length+\" valid\"} />\n ${n.length?d`
${n.slice(0,8).map(s=>{let o=s.tampered?\"tampered\":s.secondsRemaining<300?\"expiring\":\"\";return d`
\n
${s.key}${s.tampered?\" \\u26A0 TAMPERED\":\"\"}${ye(s.secondsRemaining)} left
\n
${s.reason||\"(no reason)\"}
\n
scope:${s.scope}by:${s.grantedBy}
\n
`})}
`:d`<${E} cta=${'$ qring approve KEY --for 1800 --reason \"...\"'}>No active approvals.`}\n `}function bt({snap:t,delay:e}){let n=t.hooks||[];return d`<${S} delay=${e}>\n <${M} icon=${k.hook} title=\"Hooks\" aside=${n.filter(s=>s.enabled).length+\" enabled / \"+n.length} />\n ${n.length?d`
${n.slice(0,8).map(s=>d`
\n ${s.type}\n ${s.description||s.matchSummary}\n ${s.id}\n
`)}
`:d`<${E} cta=${'$ qring hook add --tag payments --action rotate --exec \"...\"'}>No hooks registered.`}\n `}function xt({snap:t,delay:e}){return d`<${S} delay=${e}>\n <${M} icon=${k.memory} title=\"Agent Memory\" />\n
\n ${t.memoryKeys||0}\n memory key${t.memoryKeys===1?\"\":\"s\"} encrypted at ~/.config/q-ring/agent-memory.enc\n
\n `}function wt({snap:t,delay:e}){let n=t.anomalies||[];if(!n.length)return d`<${S} delay=${e} wide=${!0}>\n <${M} icon=${k.anomaly} title=\"Anomaly Alerts\" aside=\"all clear\" asideStyle=\"color:var(--green)\" />\n <${E}>No anomalies detected — quantum field is stable.\n `;let s=o=>o===\"tampered\"?\"$ qring audit:verify\":o===\"burst\"?\"$ qring audit --key \\u2026 --action read\":o===\"unusual-hour\"?'$ qring audit --action read --since \"1am\"':\"$ qring audit --limit 100\";return d`<${S} delay=${e} wide=${!0}>\n <${M} icon=${k.anomaly} title=\"Anomaly Alerts\" aside=${n.length+\" active\"} asideStyle=\"color:var(--danger)\" />\n
${n.map((o,a)=>d`
\n
${o.type}
\n
${o.description}
\n
${s(o.type)}
\n
`)}
\n `}function Ct(t,e){let n=e.trim().toLowerCase();return n?n===\"expired\"?t.filter(s=>s.decay&&s.decay.isExpired):n===\"stale\"?t.filter(s=>s.decay&&s.decay.isStale&&!s.decay.isExpired):n===\"protected\"?t.filter(s=>s.requiresApproval):t.filter(s=>[s.key,s.scope,s.type,s.provider||\"\",s.defaultEnv||\"\",(s.environments||[]).join(\" \"),(s.tags||[]).join(\" \")].join(\" \").toLowerCase().indexOf(n)!==-1):t.slice()}function Mt(t,e){let n=e.dir===\"asc\"?1:-1;return t.sort((s,o)=>{let a,l;switch(e.col){case\"key\":a=s.key,l=o.key;break;case\"scope\":a=s.scope,l=o.scope;break;case\"env\":a=s.defaultEnv||(s.environments||[])[0]||\"\",l=o.defaultEnv||(o.environments||[])[0]||\"\";break;case\"type\":a=s.type,l=o.type;break;case\"decay\":a=s.decay&&s.decay.secondsRemaining!=null?s.decay.secondsRemaining:1/0,l=o.decay&&o.decay.secondsRemaining!=null?o.decay.secondsRemaining:1/0;break;case\"tags\":a=(s.tags||[]).join(\",\"),l=(o.tags||[]).join(\",\");break;default:a=s.lastAccessedAt?new Date(s.lastAccessedAt).getTime():0,l=o.lastAccessedAt?new Date(o.lastAccessedAt).getTime():0}return al?1*n:0})}function St({snap:t,query:e,setQuery:n,sort:s,setSort:o,searchRef:a}){let l=t.secrets||[],u=te(()=>Mt(Ct(l,e),s),[l,e,s]),_=[{id:\"key\",label:\"Key\"},{id:\"scope\",label:\"Scope\"},{id:\"env\",label:\"Env\"},{id:\"type\",label:\"Type\"},{id:\"decay\",label:\"Decay\"},{id:\"tags\",label:\"Tags\"},{id:\"lastAccessedAt\",label:\"Last read\"}],c=r=>{s.col===r?o({col:r,dir:s.dir===\"asc\"?\"desc\":\"asc\"}):o({col:r,dir:r===\"key\"?\"asc\":\"desc\"})};return d`
<${S} wide=${!0} style=\"margin-bottom:16px\">\n <${M} icon=${k.key} title=\"Secrets\" aside=${u.length+\" of \"+l.length} />\n
\n \n \n \n \n \n
\n
\n ${u.length?d`\n ${_.map(r=>d``)}\n ${u.map(r=>{let p=r.type===\"superposition\"?(r.environments||[]).map(f=>d`${f}${f===r.defaultEnv?\" \\u2713\":\"\"}`):d``,i=r.decay&&r.decay.timeRemaining?(()=>{let f=Math.min(r.decay.lifetimePercent,100),h=Xe(f,r.decay.isExpired);return d`${r.decay.isExpired?\"expired\":r.decay.timeRemaining}`})():d``,v=r.tags||[];return d`\n \n \n \n \n \n \n \n `})}\n
c(r.id)}>${r.label}${s.dir===\"asc\"?\"\\u25B2\":\"\\u25BC\"}
${r.key}${r.requiresApproval?d`<${L} svg=${k.lock} />`:null}${r.scope}${p}${r.type}${i}${v.slice(0,3).map(f=>d`${f}`)}${v.length>3?\"+\"+(v.length-3):\"\"}${et(r.lastAccessedAt)}
`:d`
No secrets match the filter.
`}\n
\n
`}function At({snap:t,delay:e,filter:n,setFilter:s}){let o=t.audit||[],a=t.auditMetrics||{byAction:{},bySource:{}},l=o.filter(p=>{if(n.action&&p.action!==n.action||n.source&&p.source!==n.source)return!1;let i=n.text.trim().toLowerCase();return!(i&&[p.key||\"\",p.action,p.source,p.detail||\"\",p.scope||\"\",p.env||\"\"].join(\" \").toLowerCase().indexOf(i)===-1)}),u=[\"read\",\"write\",\"delete\",\"rotate\",\"generate\",\"entangle\",\"tunnel\",\"teleport\",\"approve\",\"revoke\",\"policy_deny\",\"collapse\",\"export\"],_=[\"cli\",\"mcp\",\"agent\",\"ci\",\"hook\",\"api\"],c=(p,i)=>s({...n,[p]:n[p]===i?\"\":i}),r=t.auditChain;return d`<${S} delay=${e} wide=${!0}>\n <${M} icon=${k.audit} title=\"Audit Log (24h)\" aside=${l.length+\" of \"+o.length+\" shown \\xB7 window \"+(a.windowSeconds||86400)/3600+\"h\"} />\n ${r&&r.totalEvents>0?d`
\n ${r.intact?d`⛓ chain intact · ${r.totalEvents} events verified`:d`⛓ chain BROKEN at event #${r.brokenAt} · ${r.validEvents}/${r.totalEvents} valid — run $ qring audit:verify`}\n
`:null}\n
\n ${u.filter(p=>a.byAction[p]).length?u.filter(p=>a.byAction[p]).map(p=>d`c(\"action\",p)}>${p}${a.byAction[p]}`):d`no actions`}\n
\n ${_.filter(p=>a.bySource[p]).length?d`
${_.filter(p=>a.bySource[p]).map(p=>d`c(\"source\",p)}>${p}${a.bySource[p]}`)}
`:null}\n
\n \n \n
\n ${l.length?d`
${l.slice(0,80).map((p,i)=>d`
\n ${Ze(p.timestamp)}\n ${p.action}\n ${p.source}\n ${p.key||\"\\u2014\"} ${p.detail||\"\"}\n
`)}
`:d`<${E}>No audit events match the filter.`}\n `}function Et(){let[t,e]=N(null),[n,s]=N(!1),[o,a]=N(\"connecting\"),[l,u]=N(\"\"),[_,c]=N({col:\"lastAccessedAt\",dir:\"desc\"}),[r,p]=N({action:\"\",source:\"\",text:\"\"}),[,i]=N(0),v=ve(n);v.current=n;let f=ve(null),h=he(g=>{try{e(typeof g==\"string\"?JSON.parse(g):g)}catch{}},[]),y=he(()=>{fetch(\"/api/status\"+fe,{cache:\"no-store\"}).then(g=>g.json()).then(h).catch(()=>{})},[h]);ee(()=>{let g=new EventSource(\"/events\"+fe);return g.onopen=()=>a(\"live\"),g.onmessage=w=>{v.current||h(w.data)},g.onerror=()=>a(\"reconnecting\"),()=>g.close()},[h]),ee(()=>{let g=setInterval(()=>i(w=>w+1),5e3);return()=>clearInterval(g)},[]),ee(()=>{let g=w=>{let P=w.target&&w.target.tagName;if(P===\"INPUT\"||P===\"TEXTAREA\"){w.key===\"Escape\"&&w.target.blur();return}w.key===\"/\"?(w.preventDefault(),f.current&&f.current.focus()):w.key===\"p\"||w.key===\"P\"?s(H=>!H):(w.key===\"r\"||w.key===\"R\")&&y()};return document.addEventListener(\"keydown\",g),()=>document.removeEventListener(\"keydown\",g)},[y]);let m=t?\"updated \"+et(t.timestamp):\"\\u2014\";if(!t)return d`
\n <${Qe} snap=${null} paused=${n} connState=${o} onPause=${()=>s(g=>!g)} onRefresh=${y} lastUpdate=${m} />\n

Connecting to the live quantum stream…

\n
`;let $=t.auditMetrics||{total:0};return d`
\n <${Qe} snap=${t} paused=${n} connState=${o} onPause=${()=>s(g=>!g)} onRefresh=${y} lastUpdate=${m} />\n <${pt} snap=${t} />\n
\n <${_t} snap=${t} delay=${0} />\n <${vt} snap=${t} delay=${60} />\n <${ht} snap=${t} delay=${120} />\n <${ft} snap=${t} delay=${180} />\n
\n <${St} snap=${t} query=${l} setQuery=${u} sort=${_} setSort=${c} searchRef=${f} />\n
\n <${yt} snap=${t} delay=${0} />\n <${$t} snap=${t} delay=${60} />\n <${mt} snap=${t} delay=${120} />\n <${gt} snap=${t} delay=${180} />\n
\n
\n <${kt} snap=${t} delay=${0} />\n <${bt} snap=${t} delay=${60} />\n <${xt} snap=${t} delay=${120} />\n
\n
\n <${wt} snap=${t} delay=${0} />\n <${At} snap=${t} delay=${60} filter=${r} setFilter=${p} />\n
\n

\n ${\"q-ring v\"+t.version+\" \\xB7 snapshot \"+Ze(t.timestamp)+\" \\xB7 \"+($.total||0)+\" audit events in last 24h\"}\n · keyboard: / search · P pause · R refresh\n

\n
`}He(d`<${Et} />`,document.getElementById(\"app\"));})();"; diff --git a/src/core/envelope.ts b/src/core/envelope.ts index dccadbb..cf95084 100644 --- a/src/core/envelope.ts +++ b/src/core/envelope.ts @@ -251,7 +251,7 @@ export function checkDecay(envelope: QuantumEnvelope): DecayStatus { const secondsRemaining = Math.floor(remaining / 1000); - let timeRemaining: string | null = null; + let timeRemaining = "expired"; if (remaining > 0) { const days = Math.floor(remaining / 86400000); const hours = Math.floor((remaining % 86400000) / 3600000); @@ -260,8 +260,6 @@ export function checkDecay(envelope: QuantumEnvelope): DecayStatus { if (days > 0) timeRemaining = `${days}d ${hours}h`; else if (hours > 0) timeRemaining = `${hours}h ${minutes}m`; else timeRemaining = `${minutes}m`; - } else { - timeRemaining = "expired"; } return { diff --git a/src/core/provision.ts b/src/core/provision.ts index 1fc3e12..57e7b0e 100644 --- a/src/core/provision.ts +++ b/src/core/provision.ts @@ -95,7 +95,7 @@ const awsStsProvider: JitProvider = { expiresAt: creds.Expiration }; } catch (err) { - throw new Error(`AWS STS provision failed: ${err instanceof Error ? err.message : String(err)}`); + throw new Error(`AWS STS provision failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err }); } } }; @@ -175,7 +175,7 @@ req.end(); expiresAt: new Date(Date.now() + expiresInSeconds * 1000).toISOString() }; } catch (err) { - throw new Error(`HTTP provision failed: ${err instanceof Error ? err.message : String(err)}`); + throw new Error(`HTTP provision failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err }); } } }; From 98ba3dbfc713d2bf8b8cba854468183b5f2dd26e Mon Sep 17 00:00:00 2001 From: I4cTime Date: Sat, 11 Jul 2026 01:31:21 -0500 Subject: [PATCH 08/16] release: v0.13.0 (#76) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version 0.13.0 staged via pnpm run release:prepare minor — package.json, plugin manifests, server.json, SECURITY.md bumped in lockstep; CHANGELOG [Unreleased] rolled into [0.13.0] — 2026-07-11. Co-authored-by: Claude Fable 5 --- .claude-plugin/marketplace.json | 4 ++-- .cursor-plugin/marketplace.json | 2 +- CHANGELOG.md | 2 ++ SECURITY.md | 4 ++-- claude-code-plugin/.claude-plugin/plugin.json | 2 +- cursor-plugin/.cursor-plugin/plugin.json | 2 +- package.json | 2 +- server.json | 4 ++-- 8 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2c1ff62..0729885 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,14 +6,14 @@ }, "metadata": { "description": "Quantum keyring for AI coding tools — secrets, superposition, entanglement, MCP.", - "version": "0.12.0" + "version": "0.13.0" }, "plugins": [ { "name": "qring", "source": "./claude-code-plugin", "description": "Quantum keyring for AI agents — manage secrets, scan for leaks, rotate keys, and enforce policy directly from Claude Code.", - "version": "0.12.0", + "version": "0.13.0", "author": { "name": "I4cTime" }, diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 3c2e7e7..f824e6c 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -12,7 +12,7 @@ "name": "qring", "source": "cursor-plugin", "description": "Quantum keyring for AI agents — manage secrets, scan for leaks, rotate keys, and enforce policy directly from Cursor.", - "version": "0.12.0", + "version": "0.13.0", "keywords": [ "secrets", "keyring", diff --git a/CHANGELOG.md b/CHANGELOG.md index 0046cc1..62b5d62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.13.0] — 2026-07-11 + ### Dependencies - **Fixed GHSA-h67p-54hq-rp68** (js-yaml quadratic-complexity DoS, dev-scope via eslint) with a `js-yaml >=4.2.0` override — resolves to 4.3.0. - **Majors:** `commander` 14→15 (CLI verified end-to-end), `eslint` 9→10 (three findings from newly-default rules fixed in code), `@types/node` 25→26; GitHub Actions `checkout` v7, `setup-node` v6, `pnpm/action-setup` v6, `codeql-action` v4. diff --git a/SECURITY.md b/SECURITY.md index c1ef67e..82ad4f0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,8 @@ | Version | Supported | |---------|-----------| -| 0.12.x | Yes | -| < 0.12 | No | +| 0.13.x | Yes | +| < 0.13 | No | ## Reporting a Vulnerability diff --git a/claude-code-plugin/.claude-plugin/plugin.json b/claude-code-plugin/.claude-plugin/plugin.json index 98571cb..700f89c 100644 --- a/claude-code-plugin/.claude-plugin/plugin.json +++ b/claude-code-plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "qring", "description": "Quantum keyring for AI agents — manage secrets, scan for leaks, rotate keys, and enforce policy directly from Claude Code.", - "version": "0.12.0", + "version": "0.13.0", "author": { "name": "I4cTime" }, diff --git a/cursor-plugin/.cursor-plugin/plugin.json b/cursor-plugin/.cursor-plugin/plugin.json index efae633..260bb5b 100644 --- a/cursor-plugin/.cursor-plugin/plugin.json +++ b/cursor-plugin/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "qring", "description": "Quantum keyring for AI agents — manage secrets, scan for leaks, rotate keys, and enforce policy directly from Cursor.", - "version": "0.12.0", + "version": "0.13.0", "author": { "name": "I4cTime" }, diff --git a/package.json b/package.json index 4d3e0c2..b99dc57 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@i4ctime/q-ring", - "version": "0.12.0", + "version": "0.13.0", "mcpName": "io.github.I4cTime/q-ring", "description": "Quantum keyring for AI coding tools — Cursor, Kiro, Claude Code. Secrets, superposition, entanglement, MCP.", "type": "module", diff --git a/server.json b/server.json index 0af9cb2..d15cf03 100644 --- a/server.json +++ b/server.json @@ -6,12 +6,12 @@ "url": "https://github.com/I4cTime/quantum_ring", "source": "github" }, - "version": "0.12.0", + "version": "0.13.0", "packages": [ { "registryType": "npm", "identifier": "@i4ctime/q-ring", - "version": "0.12.0", + "version": "0.13.0", "transport": { "type": "stdio" } From 847af9be14d3d1d9b8d51b415352430ac6630ba5 Mon Sep 17 00:00:00 2001 From: I4cTime Date: Sat, 11 Jul 2026 01:47:16 -0500 Subject: [PATCH 09/16] fix(release): rename release.yml concurrency group to avoid workflow_call deadlock (#78) (#80) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v0.13.0 tag run died in 7s: release.yml held concurrency group 'publish' while calling publish.yml, which declares the same group — a reusable workflow's top-level concurrency still applies when called, so GitHub detected a parent/child deadlock and canceled the run before the GitHub Release was created (nothing was published). Parent group is now 'release'; the called publish.yml keeps 'publish', which is what actually serializes npm/MCP publishing across the event-driven and tag-driven paths. Co-authored-by: Claude Fable 5 --- .github/workflows/release.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dca5d9c..5187671 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,8 +19,13 @@ permissions: contents: write id-token: write +# NOTE: this must NOT be the "publish" group — the called publish.yml declares +# that group itself, and a reusable workflow's concurrency still applies when +# called, so parent+child sharing a group is an instant deadlock-cancel +# (bit us on the v0.13.0 tag). The child's group is what serializes actual +# publishing; this one only keeps two release runs from racing each other. concurrency: - group: publish + group: release cancel-in-progress: false jobs: From 3f23e8c05bf3692386db363e266507a232b8bf93 Mon Sep 17 00:00:00 2001 From: I4cTime Date: Sat, 11 Jul 2026 02:00:01 -0500 Subject: [PATCH 10/16] fix(release): dispatch publish/homebrew instead of workflow_call (npm OIDC) (#81) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first v0.13.0 run created the GitHub Release but npm rejected the publish with its misleading E404: npm trusted publishing validates the OIDC token's top-level workflow file, which is release.yml when publish.yml runs via workflow_call — but the trusted publisher on npmjs.com is registered for publish.yml. release.yml now dispatches publish.yml and update-homebrew.yml on the tag ref via the workflow_dispatch API (allowed with GITHUB_TOKEN; the recursion block only suppresses event-triggered runs). Dispatched runs have publish.yml as their top-level workflow, so the OIDC claim matches the existing npm configuration and the manual re-run path keeps working unchanged. - release.yml: permissions actions:write (id-token no longer needed here), dispatch step after release creation - publish.yml: drop the now-unused workflow_call trigger - update-homebrew.yml: workflow_call -> workflow_dispatch (it already polls npm for up to 5 min, so dispatch order doesn't matter) - docs/releasing.md: document the dispatch architecture and both failure modes hit on the first tag run Co-authored-by: Claude Fable 5 --- .github/workflows/publish.yml | 21 +++++----- .github/workflows/release.yml | 56 ++++++++++++++------------- .github/workflows/update-homebrew.yml | 7 ++-- docs/releasing.md | 12 ++++-- 4 files changed, 52 insertions(+), 44 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0f46509..634eab2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,20 +5,19 @@ on: # "published" (not "created") so draft releases never trigger a publish; # matches the trigger update-homebrew.yml already uses. types: [published] - # Allow re-publishing a tag manually if the release-triggered run fails. - # Pick the tag (or any branch) when dispatching from the Actions tab. + # The tag-driven release path: release.yml dispatches this workflow on the + # tag ref. Also used to re-publish manually if a run fails — pick the tag + # (or any branch) when dispatching from the Actions tab. + # + # Deliberately NOT workflow_call: npm trusted publishing validates the OIDC + # token's top-level workflow file, and the trusted publisher on npmjs.com is + # registered for publish.yml. A called workflow presents release.yml as the + # top-level file and npm rejects the token with a misleading E404 (this + # failed the first v0.13.0 run). workflow_dispatch: inputs: ref: - description: "Git ref to publish (defaults to the current branch)" - required: false - type: string - # Called from release.yml on tag push (GITHUB_TOKEN-created releases don't - # emit events, so the release:published trigger can't fire for that path). - workflow_call: - inputs: - ref: - description: "Git ref to publish (defaults to the calling ref)" + description: "Git ref to publish (defaults to the dispatched ref)" required: false type: string diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5187671..2beef60 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,26 @@ name: Release # Pushing a v* tag drives the whole release: create the GitHub Release with -# notes from the matching CHANGELOG section, then chain the npm/MCP publish -# and the Homebrew tap update via workflow_call. +# notes from the matching CHANGELOG section, then DISPATCH the npm/MCP publish +# and the Homebrew tap update on the same tag ref. # -# Why the explicit chaining: releases created here use GITHUB_TOKEN, and -# GITHUB_TOKEN-created events do not trigger other workflows, so the -# release:published triggers on publish.yml / update-homebrew.yml would never -# fire for tag-driven releases. Those event triggers still work for releases -# created manually in the UI; every downstream step is idempotent, so the -# occasional double-run is harmless. +# Why dispatch instead of events or workflow_call: +# - Releases created here use GITHUB_TOKEN, and GITHUB_TOKEN-created events do +# not trigger other workflows, so the release:published triggers on +# publish.yml / update-homebrew.yml never fire for tag-driven releases. +# (Those event triggers still work for releases created manually in the UI.) +# - workflow_call does not work for the npm publish: npm trusted publishing +# validates the OIDC token's top-level workflow file, which would be +# release.yml for a called workflow, but the trusted publisher on npmjs.com +# is registered for publish.yml (this failed the first v0.13.0 run with +# npm's misleading E404). An explicitly dispatched run has publish.yml as +# its top-level workflow, so the claim matches. +# - Explicit workflow_dispatch API calls ARE allowed with GITHUB_TOKEN — the +# recursion prevention only suppresses event-triggered runs. +# +# Ordering: both are dispatched immediately; update-homebrew.yml already polls +# npm for up to 5 minutes for the new version, so it tolerates the publish +# finishing after it starts. Every downstream step is idempotent. on: push: @@ -17,13 +28,10 @@ on: permissions: contents: write - id-token: write + actions: write -# NOTE: this must NOT be the "publish" group — the called publish.yml declares -# that group itself, and a reusable workflow's concurrency still applies when -# called, so parent+child sharing a group is an instant deadlock-cancel -# (bit us on the v0.13.0 tag). The child's group is what serializes actual -# publishing; this one only keeps two release runs from racing each other. +# The dispatched publish.yml serializes real publishing via its own "publish" +# concurrency group; this group only keeps two release runs from racing. concurrency: group: release cancel-in-progress: false @@ -61,16 +69,10 @@ jobs: --notes-file /tmp/notes.md fi - publish: - needs: github-release - uses: ./.github/workflows/publish.yml - permissions: - contents: read - id-token: write - - homebrew: - needs: publish - uses: ./.github/workflows/update-homebrew.yml - secrets: inherit - permissions: - contents: read + - name: Dispatch publish and homebrew on this tag + env: + GH_TOKEN: ${{ github.token }} + run: | + gh workflow run publish.yml --repo "$GITHUB_REPOSITORY" --ref "$GITHUB_REF_NAME" + gh workflow run update-homebrew.yml --repo "$GITHUB_REPOSITORY" --ref "$GITHUB_REF_NAME" + echo "Dispatched publish.yml and update-homebrew.yml on $GITHUB_REF_NAME." diff --git a/.github/workflows/update-homebrew.yml b/.github/workflows/update-homebrew.yml index b82b06b..cab89ba 100644 --- a/.github/workflows/update-homebrew.yml +++ b/.github/workflows/update-homebrew.yml @@ -3,9 +3,10 @@ name: Update Homebrew Tap on: release: types: [published] - # Called from release.yml on tag push (GITHUB_TOKEN-created releases don't - # emit events). GITHUB_REF_NAME is the tag in both paths. - workflow_call: + # The tag-driven release path: release.yml dispatches this workflow on the + # tag ref (GITHUB_TOKEN-created releases don't emit events). GITHUB_REF_NAME + # is the tag in both paths — dispatch on a v* tag, never a branch. + workflow_dispatch: permissions: contents: read diff --git a/docs/releasing.md b/docs/releasing.md index 8f161f7..8238c7d 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -29,10 +29,16 @@ git push --tags `.github/workflows/release.yml` (on `v*` tags): 1. **GitHub Release** — created idempotently, with the release notes extracted from that version's CHANGELOG section. Tagging without a matching CHANGELOG section fails the run on purpose. -2. **Publish** (`publish.yml` via `workflow_call`) — npm publish with provenance (idempotent: skips if the version is already on npm), then MCP Registry publish via a pinned `mcp-publisher`. -3. **Homebrew** (`update-homebrew.yml` via `workflow_call`) — waits for the version to appear on npm, regenerates the tap formula with the new tarball sha, and pushes to `I4cTime/homebrew-tap` (skips if unchanged). +2. **Publish** (`publish.yml`, dispatched on the tag ref) — npm publish with provenance (idempotent: skips if the version is already on npm), then MCP Registry publish via a pinned `mcp-publisher`. +3. **Homebrew** (`update-homebrew.yml`, dispatched on the tag ref) — waits up to 5 minutes for the version to appear on npm, regenerates the tap formula with the new tarball sha, and pushes to `I4cTime/homebrew-tap` (skips if unchanged). -Why `workflow_call` instead of the `release: published` event for this path: the workflow creates the release with `GITHUB_TOKEN`, and GitHub does not fire workflow triggers for events created by `GITHUB_TOKEN`. The event triggers remain in place so a release created manually in the GitHub UI still publishes; every downstream step is idempotent, so overlap is harmless (a shared `publish` concurrency group serializes runs). +Why **dispatch** instead of events or `workflow_call` (both were tried and failed on the first v0.13.0 attempt): + +- The release is created with `GITHUB_TOKEN`, and GitHub does not fire workflow triggers for events created by `GITHUB_TOKEN` — so the `release: published` triggers can't fire on this path. (They remain in place so a release created manually in the UI still publishes.) +- `workflow_call` breaks npm trusted publishing: npm validates the OIDC token's *top-level* workflow file, which is `release.yml` for a called workflow, but the trusted publisher on npmjs.com is registered for `publish.yml` — npm rejects the token with a misleading `E404`. +- Explicit `workflow_dispatch` API calls **are** allowed with `GITHUB_TOKEN` (recursion prevention only suppresses event-triggered runs), and a dispatched run has `publish.yml` as its top-level workflow, so the OIDC claim matches. + +Also learned the hard way: `release.yml` must not share `publish.yml`'s concurrency group — a called/dispatched workflow's own group is what serializes publishing, and a parent holding the same group deadlocks. Every downstream step is idempotent, so overlap between the manual-release and tag-driven paths is harmless. ## Recovering from a failed release From 20446203bd9b2b6ab4ea348da1377e05963d76da Mon Sep 17 00:00:00 2001 From: I4cDeath Date: Tue, 4 Aug 2026 17:33:34 -0500 Subject: [PATCH 11/16] docs(readme): add YouTube channel badge (@qring_dev) Co-Authored-By: Claude Fable 5 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6ee9eec..585c7c6 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ [![MCP Tools](https://img.shields.io/badge/MCP_tools-44-0ea5e9?style=flat-square)](https://glama.ai/mcp/servers/I4cTime/q-ring) [![License](https://img.shields.io/npm/l/@i4ctime/q-ring?style=flat-square&color=0ea5e9)](https://github.com/I4cTime/q-ring/blob/main/LICENSE) [![Discord](https://img.shields.io/badge/discord-join%20the%20studio-5865F2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/5uEApw5uEz) +[![YouTube](https://img.shields.io/badge/youtube-%40qring__dev-FF0000?style=flat-square&logo=youtube&logoColor=white)](https://www.youtube.com/@qring_dev) q-ring MCP server From a9679668a5f71193dec9978746382ae9baa11d8c Mon Sep 17 00:00:00 2001 From: I4cDeath Date: Tue, 4 Aug 2026 17:46:51 -0500 Subject: [PATCH 12/16] =?UTF-8?q?fix(deps):=20raise=20security=20floors=20?= =?UTF-8?q?=E2=80=94=20hono=20>=3D4.12.34,=20ip-address=20>=3D10.2.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clears the 3 open Dependabot alerts: hono CORS-middleware ReDoS, and two ip-address misclassification bugs (IPv4-mapped/NAT64 + CIDR-suffix) that could weaken SSRF/trust-boundary checks. Resolves hono 4.13.0 and ip-address 10.4.0; brace-expansion floor nudged to the 5.0.9 mitigation already in the lockfile. 214/214 tests pass. Co-Authored-By: Claude Fable 5 --- package.json | 6 +++--- pnpm-lock.yaml | 30 +++++++++++++++--------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 1787a43..6de8c45 100644 --- a/package.json +++ b/package.json @@ -92,16 +92,16 @@ "overrides": { "picomatch": ">=4.0.4", "path-to-regexp": ">=8.4.0", - "hono": ">=4.12.25", + "hono": ">=4.12.34", "@hono/node-server": ">=2.0.10", "vite": "8.1.4", "qs": "6.15.2", - "ip-address": ">=10.1.1", + "ip-address": ">=10.2.2", "fast-uri": ">=3.1.4 <4", "esbuild": ">=0.28.1", "postcss": ">=8.5.18", "js-yaml": ">=4.2.0", - "brace-expansion": ">=5.0.8" + "brace-expansion": ">=5.0.9" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e4219b..b035c94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,16 +7,16 @@ settings: overrides: picomatch: '>=4.0.4' path-to-regexp: '>=8.4.0' - hono: '>=4.12.25' + hono: '>=4.12.34' '@hono/node-server': '>=2.0.10' vite: 8.1.4 qs: 6.15.2 - ip-address: '>=10.1.1' + ip-address: '>=10.2.2' fast-uri: '>=3.1.4 <4' esbuild: '>=0.28.1' postcss: '>=8.5.18' js-yaml: '>=4.2.0' - brace-expansion: '>=5.0.8' + brace-expansion: '>=5.0.9' importers: @@ -285,7 +285,7 @@ packages: resolution: {integrity: sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==} engines: {node: '>=20'} peerDependencies: - hono: '>=4.12.25' + hono: '>=4.12.34' '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} @@ -1103,8 +1103,8 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} - hono@4.12.29: - resolution: {integrity: sha512-1hNiRjawYrLq/4m3DQQjPGFg0VZkk4RjQJDff/excI6Dm9BiL75qxGrd7/c6YOxPdq6AscP3LiXhQ6fKFC1Waw==} + hono@4.13.0: + resolution: {integrity: sha512-jhunvfHWxd7J5EFfSgH4xsYJzSe/lfqbUCxiyyeaQasUsXeEHXtzVid+7EOGByc5JnFa23SSFL3Y2RV/z1T+eQ==} engines: {node: '>=16.9.0'} htm@3.1.1: @@ -1133,8 +1133,8 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ip-address@10.2.0: - resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + ip-address@10.4.0: + resolution: {integrity: sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -1886,9 +1886,9 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@hono/node-server@2.0.12(hono@4.12.29)': + '@hono/node-server@2.0.12(hono@4.13.0)': dependencies: - hono: 4.12.29 + hono: 4.13.0 '@humanfs/core@0.19.2': dependencies: @@ -1922,7 +1922,7 @@ snapshots: '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': dependencies: - '@hono/node-server': 2.0.12(hono@4.12.29) + '@hono/node-server': 2.0.12(hono@4.13.0) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 @@ -1932,7 +1932,7 @@ snapshots: eventsource-parser: 3.1.0 express: 5.2.1 express-rate-limit: 8.5.2(express@5.2.1) - hono: 4.12.29 + hono: 4.13.0 jose: 6.2.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -2537,7 +2537,7 @@ snapshots: express-rate-limit@8.5.2(express@5.2.1): dependencies: express: 5.2.1 - ip-address: 10.2.0 + ip-address: 10.4.0 express@5.2.1: dependencies: @@ -2656,7 +2656,7 @@ snapshots: dependencies: function-bind: 1.1.2 - hono@4.12.29: {} + hono@4.13.0: {} htm@3.1.1: {} @@ -2680,7 +2680,7 @@ snapshots: inherits@2.0.4: {} - ip-address@10.2.0: {} + ip-address@10.4.0: {} ipaddr.js@1.9.1: {} From 2e76ee6597d9ba5e1d06c9bcccbb2035f304c548 Mon Sep 17 00:00:00 2001 From: I4cDeath Date: Tue, 4 Aug 2026 19:25:05 -0500 Subject: [PATCH 13/16] =?UTF-8?q?docs(readme):=20badge=20parity=20?= =?UTF-8?q?=E2=80=94=20CI,=20npm=20downloads,=20@i4c=5Fstudio=20X=20badge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 585c7c6..291c8ca 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,15 @@ **OS keychain secrets for AI coding agents, over MCP.** +[![CI](https://img.shields.io/github/actions/workflow/status/I4cTime/q-ring/ci.yml?style=flat-square&label=CI)](https://github.com/I4cTime/q-ring/actions/workflows/ci.yml) [![NPM Version](https://img.shields.io/npm/v/@i4ctime/q-ring?style=flat-square&color=0ea5e9)](https://www.npmjs.com/package/@i4ctime/q-ring) +[![NPM Downloads](https://img.shields.io/npm/dm/@i4ctime/q-ring?style=flat-square&color=0ea5e9&label=downloads)](https://www.npmjs.com/package/@i4ctime/q-ring) [![Docs](https://img.shields.io/badge/docs-website-0ea5e9?style=flat-square)](https://qring.i4c.studio/docs) [![MCP Tools](https://img.shields.io/badge/MCP_tools-44-0ea5e9?style=flat-square)](https://glama.ai/mcp/servers/I4cTime/q-ring) [![License](https://img.shields.io/npm/l/@i4ctime/q-ring?style=flat-square&color=0ea5e9)](https://github.com/I4cTime/q-ring/blob/main/LICENSE) [![Discord](https://img.shields.io/badge/discord-join%20the%20studio-5865F2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/5uEApw5uEz) [![YouTube](https://img.shields.io/badge/youtube-%40qring__dev-FF0000?style=flat-square&logo=youtube&logoColor=white)](https://www.youtube.com/@qring_dev) +[![X](https://img.shields.io/badge/follow-%40i4c__studio-000000?style=flat-square&logo=x&logoColor=white)](https://x.com/i4c_studio) q-ring MCP server From 6b678c70875b3f48b6372eef61425c477285d889 Mon Sep 17 00:00:00 2001 From: I4cDeath Date: Tue, 4 Aug 2026 23:28:18 -0500 Subject: [PATCH 14/16] =?UTF-8?q?docs(readme):=20Smithery=20badge=20?= =?UTF-8?q?=E2=80=94=20verification=20backlink?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the same edit made on main via API (5abfcb1). Co-Authored-By: Claude Fable 5 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 291c8ca..910fc5e 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ [![NPM Downloads](https://img.shields.io/npm/dm/@i4ctime/q-ring?style=flat-square&color=0ea5e9&label=downloads)](https://www.npmjs.com/package/@i4ctime/q-ring) [![Docs](https://img.shields.io/badge/docs-website-0ea5e9?style=flat-square)](https://qring.i4c.studio/docs) [![MCP Tools](https://img.shields.io/badge/MCP_tools-44-0ea5e9?style=flat-square)](https://glama.ai/mcp/servers/I4cTime/q-ring) +[![smithery badge](https://smithery.ai/badge/i4ctime/q-ring)](https://smithery.ai/servers/i4ctime/q-ring) [![License](https://img.shields.io/npm/l/@i4ctime/q-ring?style=flat-square&color=0ea5e9)](https://github.com/I4cTime/q-ring/blob/main/LICENSE) [![Discord](https://img.shields.io/badge/discord-join%20the%20studio-5865F2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/5uEApw5uEz) [![YouTube](https://img.shields.io/badge/youtube-%40qring__dev-FF0000?style=flat-square&logo=youtube&logoColor=white)](https://www.youtube.com/@qring_dev) From fdaace755d257f3e39dae2d37bce0953b9135732 Mon Sep 17 00:00:00 2001 From: I4cDeath Date: Tue, 4 Aug 2026 23:42:03 -0500 Subject: [PATCH 15/16] =?UTF-8?q?docs(readme):=20shields-style=20Smithery?= =?UTF-8?q?=20badge=20=E2=80=94=20their=20badge=20endpoint=20500s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors main (b529992). Co-Authored-By: Claude Fable 5 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 910fc5e..10e559c 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ [![NPM Downloads](https://img.shields.io/npm/dm/@i4ctime/q-ring?style=flat-square&color=0ea5e9&label=downloads)](https://www.npmjs.com/package/@i4ctime/q-ring) [![Docs](https://img.shields.io/badge/docs-website-0ea5e9?style=flat-square)](https://qring.i4c.studio/docs) [![MCP Tools](https://img.shields.io/badge/MCP_tools-44-0ea5e9?style=flat-square)](https://glama.ai/mcp/servers/I4cTime/q-ring) -[![smithery badge](https://smithery.ai/badge/i4ctime/q-ring)](https://smithery.ai/servers/i4ctime/q-ring) +[![Smithery](https://img.shields.io/badge/smithery-i4ctime%2Fq--ring-0ea5e9?style=flat-square)](https://smithery.ai/servers/i4ctime/q-ring) [![License](https://img.shields.io/npm/l/@i4ctime/q-ring?style=flat-square&color=0ea5e9)](https://github.com/I4cTime/q-ring/blob/main/LICENSE) [![Discord](https://img.shields.io/badge/discord-join%20the%20studio-5865F2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/5uEApw5uEz) [![YouTube](https://img.shields.io/badge/youtube-%40qring__dev-FF0000?style=flat-square&logo=youtube&logoColor=white)](https://www.youtube.com/@qring_dev) From 62646a46c1eebeb7f89e2b86fc6fee29674b3b71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:08:08 +0000 Subject: [PATCH 16/16] chore(deps-dev): bump typescript from 5.9.3 to 6.0.3 Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.3. - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.3) --- updated-dependencies: - dependency-name: typescript dependency-version: 6.0.3 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 88 +++++++++++++++++++++++++------------------------- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index cb8f278..3500ad7 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "preact": "^10.29.7", "prettier": "^3.9.5", "tsup": "^8.5.1", - "typescript": "^5.9.3", + "typescript": "^6.0.3", "typescript-eslint": "^8.63.0", "vite": "8.1.4", "vitest": "^4.1.10" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b035c94..f0efbe3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,13 +61,13 @@ importers: version: 3.9.5 tsup: specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.25)(typescript@5.9.3) + version: 8.5.1(postcss@8.5.25)(typescript@6.0.3) typescript: - specifier: ^5.9.3 - version: 5.9.3 + specifier: ^6.0.3 + version: 6.0.3 typescript-eslint: specifier: ^8.63.0 - version: 8.63.0(eslint@10.7.0)(typescript@5.9.3) + version: 8.63.0(eslint@10.7.0)(typescript@6.0.3) vite: specifier: 8.1.4 version: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1) @@ -1621,8 +1621,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true @@ -2152,40 +2152,40 @@ snapshots: dependencies: undici-types: 8.3.0 - '@typescript-eslint/eslint-plugin@8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.7.0)(typescript@5.9.3))(eslint@10.7.0)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.7.0)(typescript@6.0.3))(eslint@10.7.0)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.63.0(eslint@10.7.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.63.0(eslint@10.7.0)(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.63.0 - '@typescript-eslint/type-utils': 8.63.0(eslint@10.7.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.63.0(eslint@10.7.0)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.63.0(eslint@10.7.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.63.0(eslint@10.7.0)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.63.0 eslint: 10.7.0 ignore: 7.0.6 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.63.0(eslint@10.7.0)(typescript@5.9.3)': + '@typescript-eslint/parser@8.63.0(eslint@10.7.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.63.0 '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.63.0 debug: 4.4.3 eslint: 10.7.0 - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.63.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.63.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@6.0.3) '@typescript-eslint/types': 8.63.0 debug: 4.4.3 - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -2194,47 +2194,47 @@ snapshots: '@typescript-eslint/types': 8.63.0 '@typescript-eslint/visitor-keys': 8.63.0 - '@typescript-eslint/tsconfig-utils@8.63.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.63.0(typescript@6.0.3)': dependencies: - typescript: 5.9.3 + typescript: 6.0.3 - '@typescript-eslint/type-utils@8.63.0(eslint@10.7.0)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.63.0(eslint@10.7.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.63.0(eslint@10.7.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.63.0(eslint@10.7.0)(typescript@6.0.3) debug: 4.4.3 eslint: 10.7.0 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.63.0': {} - '@typescript-eslint/typescript-estree@8.63.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.63.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.63.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@5.9.3) + '@typescript-eslint/project-service': 8.63.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@6.0.3) '@typescript-eslint/types': 8.63.0 '@typescript-eslint/visitor-keys': 8.63.0 debug: 4.4.3 minimatch: 10.2.5 semver: 7.8.5 tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.63.0(eslint@10.7.0)(typescript@5.9.3)': + '@typescript-eslint/utils@8.63.0(eslint@10.7.0)(typescript@6.0.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0) '@typescript-eslint/scope-manager': 8.63.0 '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) eslint: 10.7.0 - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -3090,16 +3090,16 @@ snapshots: tree-kill@1.2.2: {} - ts-api-utils@2.5.0(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: - typescript: 5.9.3 + typescript: 6.0.3 ts-interface-checker@0.1.13: {} tslib@2.8.1: optional: true - tsup@8.5.1(postcss@8.5.25)(typescript@5.9.3): + tsup@8.5.1(postcss@8.5.25)(typescript@6.0.3): dependencies: bundle-require: 5.1.0(esbuild@0.28.1) cac: 6.7.14 @@ -3120,7 +3120,7 @@ snapshots: tree-kill: 1.2.2 optionalDependencies: postcss: 8.5.25 - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - jiti - supports-color @@ -3137,18 +3137,18 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - typescript-eslint@8.63.0(eslint@10.7.0)(typescript@5.9.3): + typescript-eslint@8.63.0(eslint@10.7.0)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.7.0)(typescript@5.9.3))(eslint@10.7.0)(typescript@5.9.3) - '@typescript-eslint/parser': 8.63.0(eslint@10.7.0)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.63.0(eslint@10.7.0)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.7.0)(typescript@6.0.3))(eslint@10.7.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.63.0(eslint@10.7.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.63.0(eslint@10.7.0)(typescript@6.0.3) eslint: 10.7.0 - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - typescript@5.9.3: {} + typescript@6.0.3: {} ufo@1.6.4: {}