diff --git a/docs/remote-diagnostics-setup.md b/docs/remote-diagnostics-setup.md new file mode 100644 index 0000000..bcbf00c --- /dev/null +++ b/docs/remote-diagnostics-setup.md @@ -0,0 +1,126 @@ +# Remote Diagnostics Setup (read-only) + +This guide explains how to let Claude Code run **read-only** diagnostics against a +Multi-Hoster-Uploader instance running on a remote Windows server, through the local +**stdio MCP gateway** in `gateway/`. + +The gateway is an MCP server to Claude and a WebSocket client to a diagnostic agent inside the +app. It never touches the screen, never injects input, and never writes anything on the server. + +--- + +## 1. Enable diagnostics on the server and copy the code + +On the **server** (the machine running the app): + +1. Open the app's **Settings**. +2. Enable **"Diagnose-Zugriff"**. +3. Copy the connection **code**. It looks like `mhu1_`. + +The code carries a one-time auth **token** (and, for TLS, the server cert fingerprint). It does +**not** carry a host — you supply the host yourself when you connect. Treat the code as a +**secret**: anyone with the code and network reach to the agent can read diagnostics. + +The agent's **safe default is to bind to `127.0.0.1`** (loopback only). It is not exposed to the +network. You reach it through a tunnel (next step). + +--- + +## 2. Reach the agent over a tunnel (SSH local port-forward or WireGuard) + +Because the agent binds to `127.0.0.1` on the server, open a tunnel from your machine to the +server's loopback. The agent's default port is `9110`. + +### SSH local port-forward (recommended) + +``` +ssh -L 9110:127.0.0.1:9110 user@server +``` + +Leave that session open. Now `127.0.0.1:9110` on **your** machine is forwarded to +`127.0.0.1:9110` on the **server**. You connect Claude to **`127.0.0.1`** (your local end of the +tunnel), not the server's public IP. + +### WireGuard (alternative) + +Bring up a WireGuard tunnel to the server, then connect to the server's WireGuard address (or, if +you forward loopback over the tunnel, `127.0.0.1`). Use whichever address resolves to the agent's +`127.0.0.1:9110` on the server. + +> Only expose the agent directly on a LAN/VPN IP if you fully trust that network segment. The +> default loopback + tunnel is the secure choice. + +--- + +## 3. Install the gateway and register it with Claude (one time) + +On the machine running Claude Code: + +``` +cd gateway +npm install +``` + +Register the gateway as an MCP server (one time): + +``` +claude mcp add --transport stdio mhu-diag -- node "C:\Users\ploet\Desktop\Claude Projekte\multi-hoster-uploader\gateway\index.js" +``` + +Adjust the absolute path if your checkout lives elsewhere. + +--- + +## 4. Usage + +With the tunnel up, tell Claude: + +``` +server prod-3 at 127.0.0.1, code mhu1_<...> +``` + +Claude will: + +1. call `connect_server(code:"mhu1_...", host:"127.0.0.1")`, +2. immediately read the app version via `get_system_info`, +3. remember the server under its label in `registry.json`. + +Next time you can just say: + +``` +connect_server(label:"prod-3") +``` + +with no code — the gateway dials it from the registry. Then ask Claude "what's wrong?" — it will +typically start with **`server_health`**, the one-shot hub that aggregates errors, queue, rotation +and system info in a single call. Other read-only tools: `read_log`, `list_logs`, `list_errors`, +`get_queue_state`, `get_history`, `get_config_redacted`, `get_rotation_state`, `get_app_events`. + +--- + +## 5. Failure → meaning + +When a connect or a request fails, the gateway returns a human-readable cause. Quick reference: + +| Symptom / signal | What it means / what to do | +| -------------------------------------- | ----------------------------------------------------------------------- | +| `ECONNREFUSED` | app not running / wrong port / inbound firewall closed | +| `ETIMEDOUT` | firewall DROP / NAT not forwarded / tunnel down | +| close code **4001** | auth timeout, retry | +| close code **4002** | stale or rotated code — re-copy the current code from the server | +| close code **4003** | brute-force lockout, wait 60s | +| connected but no `auth-ok` | old app version without the diagnostic agent — update that server | +| wss fingerprint mismatch | server cert changed (reinstalled?) — re-copy the code | + +If you tunnel and still get `ECONNREFUSED`, check that the SSH session is up and that the agent is +actually listening on `127.0.0.1:9110` on the server (Diagnose-Zugriff enabled). + +--- + +## 6. What this is — and is not + +- It is **READ-ONLY**. It reads logs, errors, queue/history, redacted config, rotation and system + info. **No screen capture. No input injection. No writes or config changes.** +- The **code is a secret** (it carries the auth token). Don't paste it into public channels. +- `gateway/registry.json` stores tokens for remembered servers and is **git-ignored** — never + commit it. diff --git a/gateway/.gitignore b/gateway/.gitignore new file mode 100644 index 0000000..5bf9bec --- /dev/null +++ b/gateway/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +registry.json diff --git a/gateway/README.md b/gateway/README.md new file mode 100644 index 0000000..4b2aba0 --- /dev/null +++ b/gateway/README.md @@ -0,0 +1,56 @@ +# mhu-diagnostics-gateway + +A standalone local **stdio MCP gateway** for remote, **read-only** diagnostics of the +Multi-Hoster-Uploader app. + +It is two things at once: + +- an **MCP server** to Claude Code (stdio transport), exposing read-only diagnostic tools, and +- a plain **WebSocket client** to a diagnostic agent running inside the Electron app on a + remote Windows server. + +The operator enables "Diagnose-Zugriff" on a server, copies the connection **code**, and tells +Claude `server at , code `. Claude calls `connect_server(code, host)` and then +the read-only diagnostic tools. After the first successful connect the server is remembered under +its label, so later you can just say `connect_server(label:"prod-3")` with no code. + +This package is fully self-contained. It does **not** import anything from the parent Electron app +and is **not** part of the app build. + +## Install + +``` +cd gateway +npm install +``` + +Requires Node >= 18. + +## Register with Claude Code (one time) + +``` +claude mcp add --transport stdio mhu-diag -- node "C:\Users\ploet\Desktop\Claude Projekte\multi-hoster-uploader\gateway\index.js" +``` + +Adjust the absolute path if you cloned the repo elsewhere. + +## Usage + +In Claude Code, tell Claude: + +``` +server prod-3 at 127.0.0.1, code mhu1_<...> +``` + +Claude will call `connect_server` and then diagnostic tools such as `server_health` +(the one-shot "what's wrong" hub), `read_log`, `list_errors`, `get_queue_state`, +`get_rotation_state`, and so on. + +## Security + +- **Read-only.** No screen access, no input injection, no writes. Only reads logs, errors, + queue/history/config (redacted), rotation and system info. +- The **code is a secret** — it carries the auth token. Do not paste it anywhere public. +- The safe default is to reach the agent over `127.0.0.1` via an SSH local port-forward or + WireGuard. See `docs/remote-diagnostics-setup.md`. +- `registry.json` stores tokens and is **git-ignored** — never commit it. diff --git a/gateway/agent-client.js b/gateway/agent-client.js new file mode 100644 index 0000000..eb2d962 --- /dev/null +++ b/gateway/agent-client.js @@ -0,0 +1,219 @@ +import WebSocket from 'ws'; +import { randomUUID } from 'node:crypto'; + +const AUTH_TIMEOUT_MS = 6000; +const REQUEST_TIMEOUT_MS = 30000; + +const CLOSE_CODE_GUIDANCE = { + 4001: 'auth timeout, retry', + 4002: 'stale or rotated code — re-copy the current code from the server', + 4003: 'brute-force lockout, wait 60s', +}; + +function normalizeFingerprint(fp) { + if (typeof fp !== 'string') return ''; + return fp.replace(/:/g, '').toLowerCase(); +} + +function mapSocketError(err) { + const code = err && err.code; + if (code === 'ECONNREFUSED') { + return 'app not running / wrong port / inbound firewall closed'; + } + if (code === 'ETIMEDOUT') { + return 'firewall DROP / NAT not forwarded / tunnel down'; + } + return (err && err.message) ? err.message : String(err); +} + +function mapCloseBeforeAuth(code, sawOpen) { + if (CLOSE_CODE_GUIDANCE[code]) return CLOSE_CODE_GUIDANCE[code]; + if (sawOpen) { + return 'old app version without the diagnostic agent — update that server'; + } + return `connection closed before auth (code ${code})`; +} + +export class AgentClient { + constructor({ host, port, token, fp }) { + this.host = host; + this.port = port; + this.token = token; + this.fp = fp; + this.connected = false; + this.clientId = null; + this.ws = null; + this.authed = false; + this.sawOpen = false; + this._pending = new Map(); + this._authResolve = null; + this._authReject = null; + this._authTimer = null; + } + + connect() { + return new Promise((resolve, reject) => { + this._authResolve = resolve; + this._authReject = reject; + + const secure = typeof this.fp === 'string' && this.fp.length > 0; + const scheme = secure ? 'wss' : 'ws'; + const url = `${scheme}://${this.host}:${this.port}`; + + const pinned = secure ? normalizeFingerprint(this.fp) : ''; + const options = secure ? { rejectUnauthorized: false } : undefined; + + let ws; + try { + ws = options ? new WebSocket(url, options) : new WebSocket(url); + } catch (err) { + this._failAuth(mapSocketError(err)); + return; + } + this.ws = ws; + + this._authTimer = setTimeout(() => { + this._failAuth('auth timeout, retry'); + try { ws.close(); } catch {} + }, AUTH_TIMEOUT_MS); + + ws.on('open', () => { + this.sawOpen = true; + if (secure) { + const sock = ws._socket; + const cert = sock && typeof sock.getPeerCertificate === 'function' + ? sock.getPeerCertificate() + : null; + const actual = normalizeFingerprint(cert && cert.fingerprint256); + if (!actual || actual !== pinned) { + this._failAuth('server cert changed (reinstalled?) — re-copy the code'); + try { ws.close(); } catch {} + return; + } + } + this._send({ type: 'auth', token: this.token, role: 'diagnostic' }); + }); + + ws.on('message', (raw) => this._onMessage(raw)); + + ws.on('error', (err) => { + if (!this.authed) { + this._failAuth(mapSocketError(err)); + } else { + this._rejectAllPending(mapSocketError(err)); + } + }); + + ws.on('close', (code) => { + this.connected = false; + if (!this.authed) { + this._failAuth(mapCloseBeforeAuth(code, this.sawOpen)); + } else { + this._rejectAllPending(`connection closed (code ${code})`); + } + }); + }); + } + + _onMessage(raw) { + let msg; + try { + msg = JSON.parse(raw.toString()); + } catch { + return; + } + + if (msg.type === 'auth-ok') { + this.authed = true; + this.connected = true; + this.clientId = msg.clientId ?? null; + if (this._authTimer) { + clearTimeout(this._authTimer); + this._authTimer = null; + } + if (this._authResolve) { + const r = this._authResolve; + this._authResolve = null; + this._authReject = null; + r({ clientId: this.clientId }); + } + return; + } + + if (msg.type === 'diag-response' && msg.reqId) { + const entry = this._pending.get(msg.reqId); + if (!entry) return; + this._pending.delete(msg.reqId); + clearTimeout(entry.timer); + if (msg.ok) { + entry.resolve({ ok: true, data: msg.data }); + } else { + entry.resolve({ ok: false, error: msg.error ?? 'unknown agent error' }); + } + } + } + + request(op, args) { + return new Promise((resolve) => { + if (!this.connected || !this.ws || this.ws.readyState !== WebSocket.OPEN) { + resolve({ ok: false, error: 'not connected to a diagnostic agent' }); + return; + } + const reqId = randomUUID(); + const timer = setTimeout(() => { + if (this._pending.has(reqId)) { + this._pending.delete(reqId); + resolve({ ok: false, error: `request timed out after ${REQUEST_TIMEOUT_MS}ms (op: ${op})` }); + } + }, REQUEST_TIMEOUT_MS); + this._pending.set(reqId, { resolve, timer }); + try { + this._send({ type: 'diag-request', reqId, op, args: args ?? {} }); + } catch (err) { + this._pending.delete(reqId); + clearTimeout(timer); + resolve({ ok: false, error: mapSocketError(err) }); + } + }); + } + + close() { + this.connected = false; + this.authed = false; + if (this._authTimer) { + clearTimeout(this._authTimer); + this._authTimer = null; + } + this._rejectAllPending('connection closed by client'); + if (this.ws) { + try { this.ws.removeAllListeners(); } catch {} + try { this.ws.close(); } catch {} + this.ws = null; + } + } + + _send(obj) { + this.ws.send(JSON.stringify(obj)); + } + + _failAuth(error) { + if (this._authTimer) { + clearTimeout(this._authTimer); + this._authTimer = null; + } + if (this._authReject) { + const rej = this._authReject; + this._authResolve = null; + this._authReject = null; + rej(new Error(error)); + } + } + + _rejectAllPending(error) { + for (const [reqId, entry] of this._pending) { + clearTimeout(entry.timer); + entry.resolve({ ok: false, error }); + this._pending.delete(reqId); + } + } +} diff --git a/gateway/code.js b/gateway/code.js new file mode 100644 index 0000000..3d7149a --- /dev/null +++ b/gateway/code.js @@ -0,0 +1,59 @@ +const PREFIX = 'mhu1_'; + +export function encode(payload) { + if (!payload || typeof payload !== 'object') { + throw new Error('encode: payload must be an object'); + } + const json = JSON.stringify(payload); + const b64 = Buffer.from(json, 'utf8').toString('base64url'); + return PREFIX + b64; +} + +export function decode(code) { + if (typeof code !== 'string') { + throw new Error('Invalid code: expected a string'); + } + const trimmed = code.trim(); + if (!trimmed.startsWith(PREFIX)) { + throw new Error('Invalid code: missing "mhu1_" prefix'); + } + const b64 = trimmed.slice(PREFIX.length); + if (!b64) { + throw new Error('Invalid code: empty payload'); + } + + let json; + try { + json = Buffer.from(b64, 'base64url').toString('utf8'); + } catch { + throw new Error('Invalid code: not valid base64url'); + } + + let payload; + try { + payload = JSON.parse(json); + } catch { + throw new Error('Invalid code: payload is not valid JSON'); + } + + if (!payload || typeof payload !== 'object') { + throw new Error('Invalid code: payload is not an object'); + } + if (payload.v !== 1) { + throw new Error(`Invalid code: unsupported version (expected v=1, got ${payload.v})`); + } + if (typeof payload.port !== 'number' || !Number.isFinite(payload.port)) { + throw new Error('Invalid code: "port" must be a number'); + } + if (typeof payload.token !== 'string' || payload.token.length === 0) { + throw new Error('Invalid code: "token" must be a non-empty string'); + } + if (typeof payload.label !== 'string') { + throw new Error('Invalid code: "label" must be a string'); + } + if (payload.fp !== undefined && typeof payload.fp !== 'string') { + throw new Error('Invalid code: "fp" must be a string when present'); + } + + return payload; +} diff --git a/gateway/index.js b/gateway/index.js new file mode 100644 index 0000000..29b94f7 --- /dev/null +++ b/gateway/index.js @@ -0,0 +1,294 @@ +#!/usr/bin/env node +import { fileURLToPath } from 'node:url'; +import { z } from 'zod'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { decode } from './code.js'; +import { AgentClient } from './agent-client.js'; +import { loadRegistry, upsertEntry } from './registry.js'; + +const state = { + current: null, + servers: new Map(), +}; + +function result(obj) { + return { content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }] }; +} + +function guard(handler) { + return async (args) => { + try { + return result(await handler(args ?? {})); + } catch (e) { + return result({ ok: false, error: String(e && e.message ? e.message : e) }); + } + }; +} + +async function requireCurrent(op, args) { + if (!state.current || !state.current.client || !state.current.client.connected) { + return { ok: false, error: 'no server connected — call connect_server first' }; + } + return state.current.client.request(op, args ?? {}); +} + +const DIAGNOSTIC_TOOLS = [ + { + name: 'server_health', + title: 'Server health (one-shot hub)', + description: + 'THE one-shot diagnostic hub: answers "what is wrong" in a single call by aggregating recent errors, queue state, rotation and system info.', + op: 'server_health', + inputSchema: { + errorLimit: z.number().optional(), + errorSinceMs: z.number().optional(), + }, + }, + { + name: 'read_log', + title: 'Read a log file', + description: 'Read a tail of one of the app log files, optionally grep-filtered, optionally a rotated backup.', + op: 'read_log', + inputSchema: { + name: z.enum(['debug', 'fileuploader', 'accountRotation', 'crash']), + tailKb: z.number().optional(), + grep: z.string().optional(), + backup: z.number().optional(), + }, + }, + { + name: 'list_logs', + title: 'List available logs', + description: 'List the available log files and their sizes.', + op: 'list_logs', + inputSchema: {}, + }, + { + name: 'list_errors', + title: 'List recent errors', + description: 'List recent structured errors, filterable by time window, category and hoster.', + op: 'list_errors', + inputSchema: { + sinceMs: z.number().optional(), + category: z.string().optional(), + hoster: z.string().optional(), + limit: z.number().optional(), + }, + }, + { + name: 'get_queue_state', + title: 'Get queue state', + description: 'Get the upload queue state, optionally including individual jobs.', + op: 'get_queue_state', + inputSchema: { + includeJobs: z.boolean().optional(), + maxJobs: z.number().optional(), + }, + }, + { + name: 'get_history', + title: 'Get upload history', + description: 'Get recent upload history, optionally including file names and URLs.', + op: 'get_history', + inputSchema: { + limit: z.number().optional(), + includeFiles: z.boolean().optional(), + includeUrls: z.boolean().optional(), + }, + }, + { + name: 'get_config_redacted', + title: 'Get redacted config', + description: 'Get the app configuration with secrets redacted. Choose a section to narrow the output.', + op: 'get_config_redacted', + inputSchema: { + section: z.enum(['all', 'hosters', 'hosterSettings', 'globalSettings', 'rotationCursors']).optional(), + }, + }, + { + name: 'get_system_info', + title: 'Get system info', + description: 'Get system and app version info (OS, app version, uptime).', + op: 'get_system_info', + inputSchema: {}, + }, + { + name: 'get_rotation_state', + title: 'Get rotation state', + description: 'Get the per-hoster account rotation state and round-robin cursors.', + op: 'get_rotation_state', + inputSchema: {}, + }, + { + name: 'get_app_events', + title: 'Get app events', + description: 'Get recent in-app lifecycle events.', + op: 'get_app_events', + inputSchema: { + limit: z.number().optional(), + }, + }, +]; + +async function doConnect({ code, host, port, label }) { + const registry = await loadRegistry(); + + let target; + if (label && registry[label]) { + const e = registry[label]; + target = { host: e.host, port: e.port, token: e.token, fp: e.fp, label: e.label }; + } else { + if (!code) { + return { ok: false, error: 'provide a known label, or a code plus host' }; + } + let payload; + try { + payload = decode(code); + } catch (e) { + return { ok: false, error: String(e.message ?? e) }; + } + if (!host) { + return { ok: false, error: 'host is required when connecting with a code' }; + } + target = { + host, + port: typeof port === 'number' ? port : payload.port, + token: payload.token, + fp: payload.fp, + label: label || payload.label, + }; + } + + if (typeof port === 'number') target.port = port; + + const client = new AgentClient(target); + try { + await client.connect(); + } catch (e) { + return { ok: false, error: String(e.message ?? e) }; + } + + let version; + const info = await client.request('get_system_info', {}); + if (info && info.ok && info.data) { + version = info.data.version ?? info.data.appVersion ?? undefined; + } + + const id = `${target.label}@${target.host}:${target.port}`; + if (state.current && state.current.client && state.current.client !== client) { + state.current.client.close(); + } + state.servers.set(id, { id, client, target }); + state.current = { id, client, target }; + + await upsertEntry({ + host: target.host, + port: target.port, + token: target.token, + fp: target.fp, + label: target.label, + version, + lastConnectedAt: new Date().toISOString(), + }); + + return { + ok: true, + server: { label: target.label, host: target.host, port: target.port, version }, + }; +} + +export function buildServer() { + const server = new McpServer({ name: 'mhu-diagnostics-gateway', version: '1.0.0' }); + + server.registerTool( + 'list_servers', + { + title: 'List known servers', + description: 'List the registry of known diagnostic servers and which one is currently connected.', + inputSchema: {}, + }, + guard(async () => { + const registry = await loadRegistry(); + return { + ok: true, + servers: Object.values(registry), + current: state.current ? state.current.id : null, + }; + }), + ); + + server.registerTool( + 'connect_server', + { + title: 'Connect to a diagnostic server', + description: + 'Connect to a remote diagnostic agent. Use label for a known server, or code+host for a new one. The host is always supplied by you, never taken from the code.', + inputSchema: { + code: z.string().optional(), + host: z.string().optional(), + port: z.number().optional(), + label: z.string().optional(), + }, + }, + guard((args) => doConnect(args)), + ); + + server.registerTool( + 'disconnect_server', + { + title: 'Disconnect the current server', + description: 'Close the connection to the currently connected diagnostic server.', + inputSchema: {}, + }, + guard(async () => { + if (!state.current) return { ok: true, disconnected: false }; + const id = state.current.id; + try { state.current.client.close(); } catch {} + state.servers.delete(id); + state.current = null; + return { ok: true, disconnected: true, id }; + }), + ); + + server.registerTool( + 'current_server', + { + title: 'Show the current server', + description: 'Show the currently connected diagnostic target, or null if none.', + inputSchema: {}, + }, + guard(async () => { + if (!state.current) return { ok: true, current: null }; + const t = state.current.target; + return { + ok: true, + current: { id: state.current.id, label: t.label, host: t.host, port: t.port }, + }; + }), + ); + + for (const tool of DIAGNOSTIC_TOOLS) { + server.registerTool( + tool.name, + { title: tool.title, description: tool.description, inputSchema: tool.inputSchema }, + guard((args) => requireCurrent(tool.op, args)), + ); + } + + return server; +} + +async function main() { + const server = buildServer(); + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error('[mhu-diag] gateway ready (stdio MCP)'); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + main().catch((e) => { + console.error('[mhu-diag] fatal:', e); + process.exit(1); + }); +} diff --git a/gateway/package-lock.json b/gateway/package-lock.json new file mode 100644 index 0000000..25d1198 --- /dev/null +++ b/gateway/package-lock.json @@ -0,0 +1,1197 @@ +{ + "name": "mhu-diagnostics-gateway", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mhu-diagnostics-gateway", + "version": "1.0.0", + "dependencies": { + "@modelcontextprotocol/sdk": "~1.29.0", + "ws": "^8", + "zod": "^3.25.0" + }, + "bin": { + "mhu-diag": "index.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.26", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz", + "integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/gateway/package.json b/gateway/package.json new file mode 100644 index 0000000..5b2fcf8 --- /dev/null +++ b/gateway/package.json @@ -0,0 +1,18 @@ +{ + "name": "mhu-diagnostics-gateway", + "version": "1.0.0", + "private": true, + "type": "module", + "description": "Local stdio MCP gateway for remote diagnostics of the Multi-Hoster-Uploader app.", + "bin": { + "mhu-diag": "index.js" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "~1.29.0", + "ws": "^8", + "zod": "^3.25.0" + } +} diff --git a/gateway/registry.example.json b/gateway/registry.example.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/gateway/registry.example.json @@ -0,0 +1 @@ +{} diff --git a/gateway/registry.js b/gateway/registry.js new file mode 100644 index 0000000..de948d9 --- /dev/null +++ b/gateway/registry.js @@ -0,0 +1,50 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REGISTRY_PATH = join(HERE, 'registry.json'); + +export async function loadRegistry() { + try { + const raw = await readFile(REGISTRY_PATH, 'utf8'); + const parsed = JSON.parse(raw); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return {}; + } + return parsed; + } catch { + return {}; + } +} + +export async function saveRegistry(registry) { + const data = registry && typeof registry === 'object' ? registry : {}; + await writeFile(REGISTRY_PATH, JSON.stringify(data, null, 2) + '\n', 'utf8'); +} + +export async function upsertEntry(entry) { + if (!entry || typeof entry.label !== 'string' || entry.label.length === 0) { + throw new Error('upsertEntry: entry.label is required'); + } + const registry = await loadRegistry(); + registry[entry.label] = { + host: entry.host, + port: entry.port, + token: entry.token, + fp: entry.fp, + label: entry.label, + version: entry.version, + lastConnectedAt: entry.lastConnectedAt ?? new Date().toISOString(), + }; + await saveRegistry(registry); + return registry[entry.label]; +} + +export async function getEntry(label) { + if (typeof label !== 'string' || label.length === 0) return null; + const registry = await loadRegistry(); + return registry[label] ?? null; +} + +export { REGISTRY_PATH }; diff --git a/gateway/test/code.test.js b/gateway/test/code.test.js new file mode 100644 index 0000000..705803c --- /dev/null +++ b/gateway/test/code.test.js @@ -0,0 +1,62 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { encode, decode } from '../code.js'; + +test('decode(encode(x)) round-trips a full payload', () => { + const payload = { + v: 1, + port: 9110, + token: 'deadbeefcafe1234', + label: 'prod-3', + fp: 'AB:CD:EF:01:23:45:67:89', + }; + const code = encode(payload); + assert.ok(code.startsWith('mhu1_')); + assert.deepEqual(decode(code), payload); +}); + +test('decode(encode(x)) round-trips a payload without fp (ws://)', () => { + const payload = { v: 1, port: 9110, token: 'token-abc', label: 'localhost' }; + const code = encode(payload); + assert.deepEqual(decode(code), payload); +}); + +test('decode rejects a string without the mhu1_ prefix', () => { + assert.throws(() => decode('hello-world'), /missing "mhu1_" prefix/); +}); + +test('decode rejects a wrong-version payload', () => { + const bad = 'mhu1_' + Buffer.from( + JSON.stringify({ v: 2, port: 9110, token: 'x', label: 'l' }), + 'utf8', + ).toString('base64url'); + assert.throws(() => decode(bad), /unsupported version/); +}); + +test('decode rejects garbage after the prefix', () => { + assert.throws(() => decode('mhu1_!!!not-base64-or-json!!!'), /Invalid code/); +}); + +test('decode rejects an empty payload', () => { + assert.throws(() => decode('mhu1_'), /empty payload/); +}); + +test('decode rejects a non-string input', () => { + assert.throws(() => decode(null), /expected a string/); +}); + +test('decode rejects a missing token', () => { + const bad = 'mhu1_' + Buffer.from( + JSON.stringify({ v: 1, port: 9110, label: 'l' }), + 'utf8', + ).toString('base64url'); + assert.throws(() => decode(bad), /token/); +}); + +test('decode rejects a non-number port', () => { + const bad = 'mhu1_' + Buffer.from( + JSON.stringify({ v: 1, port: 'nope', token: 'x', label: 'l' }), + 'utf8', + ).toString('base64url'); + assert.throws(() => decode(bad), /port/); +}); diff --git a/gateway/test/e2e-verify.mjs b/gateway/test/e2e-verify.mjs new file mode 100644 index 0000000..014e0ef --- /dev/null +++ b/gateway/test/e2e-verify.mjs @@ -0,0 +1,124 @@ +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { writeFileSync, mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import assert from 'node:assert'; +import { encode, decode } from '../code.js'; +import { AgentClient } from '../agent-client.js'; + +const require = createRequire(import.meta.url); +const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const RemoteServer = require(join(root, 'lib', 'remote-server.js')); +const support = require(join(root, 'lib', 'support-bundle.js')); +const stats = require(join(root, 'lib', 'stats.js')); +const { createCollectors } = require(join(root, 'lib', 'diagnostics-collectors.js')); +const { createAgent } = require(join(root, 'lib', 'diagnostics-agent.js')); + +const SECRET_API = 'SUPERSECRET_apikey_9f8e7d6c5b4a'; +const SECRET_PW = 'hunter2_password_zxcv'; +const SECRET_TOKEN = 'bearer_tok_qwerty12345'; +const WEBHOOK = 'https://discord.com/api/webhooks/123456789012345678/abcDEF_secretWebhookToken-xyz'; + +const tmp = mkdtempSync(join(tmpdir(), 'mhu-e2e-')); +const debugLog = join(tmp, 'debug.log'); +const dood = join(tmp, 'doodstream-debug.log'); +writeFileSync(debugLog, [ + `[2026-06-19T10:00:00Z] starting upload with apiKey=${SECRET_API}`, + `[2026-06-19T10:00:01Z] Authorization: Bearer ${SECRET_TOKEN}`, + `[2026-06-19T10:00:02Z] posting to ${WEBHOOK}`, + `[2026-06-19T10:00:03Z] password ${SECRET_PW} used`, + `[2026-06-19T10:00:04Z] normal benign line`, +].join('\n')); +writeFileSync(dood, `[doodstream] live apiKey=${SECRET_API}\n`); + +const fakeConfig = { + hosters: { doodstream: [{ accountId: 'acc1', apiKey: SECRET_API, password: SECRET_PW }] }, + hosterSettings: {}, + globalSettings: { + webhookUrl: WEBHOOK, + diagnostics: { enabled: true, port: 9110, token: 'x'.repeat(64) }, + pendingQueue: { savedAt: '2026-06-19T09:00:00Z', queueJobs: [{ file: 'a.mp4', fileName: 'a.mp4', hoster: 'doodstream', status: 'error', error: `failed with apiKey=${SECRET_API}` }], selectedUploadHosters: ['doodstream'], selectedFiles: ['a.mp4'] }, + }, + rotationCursors: { doodstream: 1 }, + history: [{ timestamp: '2026-06-19T08:00:00Z', files: [{ name: 'a.mp4', results: [{ hoster: 'doodstream', status: 'error', error: `boom token=${SECRET_TOKEN}` }] }] }], +}; + +function getAllLogPaths() { + return { fileuploader: join(tmp, 'fileuploader.log'), debug: debugLog, accountRotation: join(tmp, 'rot.log'), doodstreamDebug: dood, crashLog: join(tmp, 'crash.log'), logDir: tmp }; +} + +const collectors = createCollectors({ + loadConfig: () => fakeConfig, + getAllLogPaths, + support, + stats, + appInfo: () => ({ version: '9.9.9' }), + systemInfo: () => ({ platform: 'win32', hostname: 'TESTHOST' }), + agentInfo: () => ({ version: '9.9.9', port: 9110 }), +}); +const agent = createAgent(collectors); + +const TOKEN = 'z'.repeat(64); +const srv = new RemoteServer(); + +const SECRETS = [SECRET_API, SECRET_PW, SECRET_TOKEN, 'abcDEF_secretWebhookToken-xyz']; + +function assertNoLeak(label, payload) { + const text = JSON.stringify(payload); + for (const s of SECRETS) { + assert.ok(!text.includes(s), `LEAK in ${label}: secret "${s.slice(0, 12)}…" appeared in response`); + } +} + +(async () => { + await srv.start({ + port: 0, + host: '127.0.0.1', + token: TOKEN, + diagnosticMode: true, + onDiagnosticRequest: (msg, _client, reply) => { + let r; + try { r = agent.handle(msg.op, msg.args); } + catch (e) { r = { ok: false, error: String(e && e.message || e) }; } + reply(r); + }, + }); + const port = srv.getPort(); + + const code = encode({ v: 1, port, token: TOKEN, label: 'e2e' }); + const payload = decode(code); + assert.equal(payload.token, TOKEN); + + const client = new AgentClient({ host: '127.0.0.1', port, token: payload.token }); + await client.connect(); + + const health = await client.request('server_health', { errorLimit: 10 }); + assert.equal(health.ok, true, 'server_health must succeed: ' + JSON.stringify(health)); + assertNoLeak('server_health', health); + assert.ok(health.data.errors, 'server_health has errors section'); + assert.ok(health.data.queue, 'server_health has queue section'); + + const log = await client.request('read_log', { name: 'debug', tailKb: 64 }); + assert.equal(log.ok, true, 'read_log debug must succeed'); + assert.ok(log.data.content.includes('normal benign line'), 'benign content preserved'); + assertNoLeak('read_log:debug', log); + + const dl = await client.request('read_log', { name: 'doodstream', tailKb: 64 }); + assert.equal(dl.ok, false, 'doodstream log MUST NOT be readable (live api keys)'); + + const trav = await client.request('read_log', { name: '../../../etc/passwd' }); + assert.equal(trav.ok, false, 'path traversal must be rejected'); + + const cfg = await client.request('get_config_redacted', { section: 'all' }); + assert.equal(cfg.ok, true, 'get_config_redacted must succeed'); + assertNoLeak('get_config_redacted', cfg); + + const writeAttempt = await client.request('save_config', { x: 1 }); + assert.equal(writeAttempt.ok, false, 'unknown/write op must be rejected by whitelist'); + + client.close(); + srv.stop(); + console.log('E2E PASS: connect → server_health/read_log/get_config_redacted succeeded; NO secrets leaked; doodstream + traversal + write rejected.'); + process.exit(0); +})().catch((e) => { console.error('E2E FAIL:', e && e.stack || e); try { srv.stop(); } catch {} process.exit(1); }); diff --git a/lib/diagnostics-collectors.js b/lib/diagnostics-collectors.js index a7649e9..7afbc9f 100644 --- a/lib/diagnostics-collectors.js +++ b/lib/diagnostics-collectors.js @@ -21,6 +21,21 @@ function createCollectors(deps) { try { return support.valueScrub(value, secrets || _secrets()); } catch { return value; } } + function _deepRedact(value, secrets) { + const s = secrets || _secrets(); + const walk = (v) => { + if (typeof v === 'string') return support.redactLogText(v, s); + if (Array.isArray(v)) return v.map(walk); + if (v && typeof v === 'object') { + const o = {}; + for (const k of Object.keys(v)) o[k] = walk(v[k]); + return o; + } + return v; + }; + try { return walk(value); } catch { return value; } + } + function _resolveLogPath(name, backup) { const key = READABLE_LOGS[name]; if (!key) return null; @@ -40,9 +55,16 @@ function createCollectors(deps) { const cfg = loadConfig(); const secrets = support.collectSecretValues(cfg); const sanitized = support.sanitizeConfig(cfg); - let pick = sanitized; - if (section !== 'all') pick = sanitized[section] !== undefined ? sanitized[section] : null; - return { section, config: _scrub(pick, secrets) }; + let pick; + let note; + if (section === 'all') { + pick = { ...sanitized }; + delete pick.history; + note = 'history omitted from config — use get_history'; + } else { + pick = sanitized[section] !== undefined ? sanitized[section] : null; + } + return { section, note, config: _deepRedact(pick, secrets) }; } function listLogs() { diff --git a/lib/support-bundle.js b/lib/support-bundle.js index bc84c1b..22a67de 100644 --- a/lib/support-bundle.js +++ b/lib/support-bundle.js @@ -44,8 +44,9 @@ function redactLogText(text, secrets) { out = out .replace(/https?:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w-]+/gi, 'https://discord.com/api/webhooks/' + REDACTED) .replace(/(authorization:\s*bearer\s+)\S+/gi, '$1' + REDACTED) + .replace(/\bbearer\s+[A-Za-z0-9._\-/+]{16,}/gi, 'bearer ' + REDACTED) .replace(/([?&](?:api_?key|key|token|access_token|password|pass)=)[^\s&"'`]+/gi, '$1' + REDACTED) - .replace(/("?\b(?:api[_-]?key|apikey|password|secret|access_token)"?\s*[:=]\s*"?)[A-Za-z0-9._\-/+]{8,}/gi, '$1' + REDACTED) + .replace(/("?\b(?:api[_-]?key|apikey|password|passwd|secret|(?:access|refresh|auth|session)[_-]?token|token|sessionid)"?\s*[:=]\s*"?)[A-Za-z0-9._\-/+]{8,}/gi, '$1' + REDACTED) .replace(/(\bset-cookie:|\bcookie:)\s*\S[^\n]*/gi, '$1 ' + REDACTED) .replace(/(\bsess(?:_?id)?\b["'=:\s]+)[A-Za-z0-9._\-]{8,}/gi, '$1' + REDACTED); return out; diff --git a/main.js b/main.js index a0f27d6..154fe46 100644 --- a/main.js +++ b/main.js @@ -2491,6 +2491,12 @@ function buildDiagnosticCode(diag, fp) { return 'mhu1_' + Buffer.from(JSON.stringify(payload)).toString('base64url'); } +function _safeDiagBindAddress(addr) { + const a = String(addr || '').trim(); + if (a === '127.0.0.1' || a === '::1') return a; + return '127.0.0.1'; +} + async function startDiagnosticAgent() { if (diagnosticAgent) { try { diagnosticAgent.stop(); } catch {} diagnosticAgent = null; } const config = configStore.load(); @@ -2505,16 +2511,17 @@ async function startDiagnosticAgent() { } if (!_diagHandler) _diagHandler = _buildDiagnosticHandler(); + const host = _safeDiagBindAddress(diag.bindAddress); diagnosticAgent = new RemoteServer(); try { await diagnosticAgent.start({ port: diag.port || 9110, - host: diag.bindAddress || '127.0.0.1', + host, token, diagnosticMode: true, onDiagnosticRequest: _diagHandler }); - debugLog(`diagnostics-agent started on ${diag.bindAddress || '127.0.0.1'}:${diagnosticAgent.getPort()}`); + debugLog(`diagnostics-agent started on ${host}:${diagnosticAgent.getPort()}`); } catch (e) { debugLog(`diagnostics-agent start failed: ${e.message}`); diagnosticAgent = null; @@ -2545,7 +2552,7 @@ ipcMain.handle('diagnostics:save-settings', async (_e, incoming) => { ...cur, enabled: !!(incoming && incoming.enabled), port: (incoming && Number(incoming.port)) || cur.port || 9110, - bindAddress: (incoming && incoming.bindAddress) || cur.bindAddress || '127.0.0.1', + bindAddress: _safeDiagBindAddress((incoming && incoming.bindAddress) || cur.bindAddress), label: (incoming && incoming.label != null) ? String(incoming.label) : cur.label }; const gs = { ...cfg.globalSettings, diagnostics: next }; diff --git a/renderer/app.js b/renderer/app.js index c37fcb3..5ce122a 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -3219,9 +3219,9 @@ function renderSettings() { +
Direkte LAN-/Internet-Bindung ist deaktiviert, bis verschlüsselter Transport (wss/TLS) verfügbar ist. Zugriff aus der Ferne läuft über einen SSH- oder VPN-Tunnel zu 127.0.0.1.
diff --git a/tests/support-bundle.test.js b/tests/support-bundle.test.js index 8745301..d39edfa 100644 --- a/tests/support-bundle.test.js +++ b/tests/support-bundle.test.js @@ -3,7 +3,7 @@ const assert = require('node:assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); -const { sanitizeConfig, collectFile, buildSupportBundleText, REDACTED } = require('../lib/support-bundle'); +const { sanitizeConfig, collectFile, buildSupportBundleText, redactLogText, REDACTED } = require('../lib/support-bundle'); test('sanitizeConfig redacts known credential keys at any nesting depth', () => { const input = { @@ -24,6 +24,26 @@ test('sanitizeConfig redacts known credential keys at any nesting depth', () => assert.strictEqual(out.globalSettings.remote.token, REDACTED); }); +test('redactLogText scrubs opaque tokens that are NOT stored config secrets', () => { + const cases = [ + 'boom token=bearer_tok_qwerty12345', + 'response auth_token: aGVsbG8td29ybGQtMTIz', + 'refresh_token = abc123DEF456ghi789', + 'using Bearer aaaabbbbccccddddeeeeffff', + 'Authorization: Bearer deadbeefcafef00dba5e' + ]; + for (const line of cases) { + const out = redactLogText(line, []); + assert.ok(out.includes(REDACTED), `expected redaction in: ${line} -> ${out}`); + assert.ok(!/qwerty12345|aGVsbG8|abc123DEF456|aaaabbbbcccc|deadbeefcafe/.test(out), `secret survived: ${out}`); + } +}); + +test('redactLogText leaves benign "token" prose alone', () => { + const benign = 'token bucket refill rate is 5 per second'; + assert.equal(redactLogText(benign, []), benign); +}); + test('sanitizeConfig does not mutate input', () => { const input = { hosters: { 'voe.sx': [{ password: 'secret' }] } }; const clone = JSON.parse(JSON.stringify(input));