From 473999c4f2751d6ba19cf8e7093a77f2271ead26 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Tue, 17 Feb 2026 20:40:31 -0700 Subject: [PATCH 01/41] feat(#249): add custom REST API endpoints backend foundation --- src/lib/db.js | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/src/lib/db.js b/src/lib/db.js index e0543567..677dd07c 100644 --- a/src/lib/db.js +++ b/src/lib/db.js @@ -281,6 +281,32 @@ db.exec(` -- Replay protection: unique delivery IDs per source CREATE UNIQUE INDEX IF NOT EXISTS idx_webhook_delivery_dedup ON webhook_deliveries(source, delivery_id) WHERE delivery_id IS NOT NULL; + + -- Custom services (user-defined REST API endpoints, #249) + CREATE TABLE IF NOT EXISTS custom_services ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + display_name TEXT NOT NULL, + description TEXT, + base_url TEXT NOT NULL, + docs_url TEXT, + category TEXT DEFAULT 'custom', + auth_config TEXT NOT NULL, + endpoints TEXT NOT NULL, + enabled INTEGER DEFAULT 1, + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS custom_service_accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + service_id INTEGER NOT NULL REFERENCES custom_services(id) ON DELETE CASCADE, + account_name TEXT NOT NULL, + credentials TEXT NOT NULL, + enabled INTEGER DEFAULT 1, + created_at TEXT DEFAULT (datetime('now')), + UNIQUE(service_id, account_name) + ); `); // ============================================ @@ -2449,3 +2475,153 @@ export function getChatMessageCount(channelId) { return row ? row.count : 0; } +// ============================================ +// Custom Services (#249) +// ============================================ + +export function createCustomService({ name, displayName, description, baseUrl, docsUrl, category, authConfig, endpoints }) { + const result = db.prepare(` + INSERT INTO custom_services (name, display_name, description, base_url, docs_url, category, auth_config, endpoints) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `).run(name, displayName, description || null, baseUrl, docsUrl || null, category || 'custom', + JSON.stringify(authConfig), JSON.stringify(endpoints || [])); + return { id: Number(result.lastInsertRowid), name }; +} + +export function getCustomService(nameOrId) { + const row = typeof nameOrId === 'number' + ? db.prepare('SELECT * FROM custom_services WHERE id = ?').get(nameOrId) + : db.prepare('SELECT * FROM custom_services WHERE name = ?').get(nameOrId); + if (!row) return null; + return { + ...row, + auth_config: JSON.parse(row.auth_config), + endpoints: JSON.parse(row.endpoints), + enabled: row.enabled === 1 + }; +} + +export function listCustomServices() { + const rows = db.prepare('SELECT * FROM custom_services ORDER BY name').all(); + return rows.map(row => ({ + ...row, + auth_config: JSON.parse(row.auth_config), + endpoints: JSON.parse(row.endpoints), + enabled: row.enabled === 1 + })); +} + +export function updateCustomService(name, updates) { + const fields = []; + const values = []; + const mapping = { + displayName: 'display_name', description: 'description', baseUrl: 'base_url', + docsUrl: 'docs_url', category: 'category', enabled: 'enabled' + }; + for (const [jsKey, dbKey] of Object.entries(mapping)) { + if (updates[jsKey] !== undefined) { + fields.push(`${dbKey} = ?`); + values.push(jsKey === 'enabled' ? (updates[jsKey] ? 1 : 0) : updates[jsKey]); + } + } + if (updates.authConfig !== undefined) { + fields.push('auth_config = ?'); + values.push(JSON.stringify(updates.authConfig)); + } + if (updates.endpoints !== undefined) { + fields.push('endpoints = ?'); + values.push(JSON.stringify(updates.endpoints)); + } + if (fields.length === 0) return; + fields.push('updated_at = datetime(\'now\')'); + values.push(name); + db.prepare(`UPDATE custom_services SET ${fields.join(', ')} WHERE name = ?`).run(...values); +} + +export function deleteCustomService(name) { + return db.prepare('DELETE FROM custom_services WHERE name = ?').run(name); +} + +// Custom service accounts +export function createCustomServiceAccount(serviceName, accountName, credentials) { + const svc = db.prepare('SELECT id FROM custom_services WHERE name = ?').get(serviceName); + if (!svc) throw new Error(`Custom service '${serviceName}' not found`); + const result = db.prepare(` + INSERT INTO custom_service_accounts (service_id, account_name, credentials) + VALUES (?, ?, ?) + `).run(svc.id, accountName, JSON.stringify(credentials)); + return { id: Number(result.lastInsertRowid), service: serviceName, account_name: accountName }; +} + +export function getCustomServiceAccount(serviceName, accountName) { + const row = db.prepare(` + SELECT csa.* FROM custom_service_accounts csa + JOIN custom_services cs ON csa.service_id = cs.id + WHERE cs.name = ? AND csa.account_name = ? + `).get(serviceName, accountName); + if (!row) return null; + return { ...row, credentials: JSON.parse(row.credentials), enabled: row.enabled === 1 }; +} + +export function listCustomServiceAccounts(serviceName) { + const rows = db.prepare(` + SELECT csa.id, csa.account_name, csa.enabled, csa.created_at + FROM custom_service_accounts csa + JOIN custom_services cs ON csa.service_id = cs.id + WHERE cs.name = ? + ORDER BY csa.account_name + `).all(serviceName); + return rows.map(r => ({ ...r, enabled: r.enabled === 1 })); +} + +export function updateCustomServiceAccount(serviceName, accountName, updates) { + const svc = db.prepare('SELECT id FROM custom_services WHERE name = ?').get(serviceName); + if (!svc) return; + if (updates.credentials !== undefined) { + db.prepare(` + UPDATE custom_service_accounts SET credentials = ? WHERE service_id = ? AND account_name = ? + `).run(JSON.stringify(updates.credentials), svc.id, accountName); + } + if (updates.enabled !== undefined) { + db.prepare(` + UPDATE custom_service_accounts SET enabled = ? WHERE service_id = ? AND account_name = ? + `).run(updates.enabled ? 1 : 0, svc.id, accountName); + } +} + +export function deleteCustomServiceAccount(serviceName, accountName) { + const svc = db.prepare('SELECT id FROM custom_services WHERE name = ?').get(serviceName); + if (!svc) return { changes: 0 }; + return db.prepare('DELETE FROM custom_service_accounts WHERE service_id = ? AND account_name = ?').run(svc.id, accountName); +} + +export function listEnabledCustomServices() { + const rows = db.prepare(` + SELECT cs.*, csa.account_name, csa.credentials as account_credentials, csa.enabled as account_enabled + FROM custom_services cs + LEFT JOIN custom_service_accounts csa ON cs.id = csa.service_id AND csa.enabled = 1 + WHERE cs.enabled = 1 + ORDER BY cs.name, csa.account_name + `).all(); + // Group by service + const services = new Map(); + for (const row of rows) { + if (!services.has(row.name)) { + services.set(row.name, { + ...row, + auth_config: JSON.parse(row.auth_config), + endpoints: JSON.parse(row.endpoints), + enabled: true, + accounts: [] + }); + } + if (row.account_name) { + services.get(row.name).accounts.push({ + account_name: row.account_name, + credentials: JSON.parse(row.account_credentials) + }); + } + } + return [...services.values()]; +} + From 09d3d7ec8f671ec4e694bd9fb67b6bff04821761 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Tue, 17 Feb 2026 20:40:32 -0700 Subject: [PATCH 02/41] feat(#249): add custom REST API endpoints backend foundation --- src/index.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/index.js b/src/index.js index 0439d114..304d89e2 100644 --- a/src/index.js +++ b/src/index.js @@ -32,6 +32,8 @@ import { setupAgentChannelProxy } from './routes/channel-agent.js'; import { validateAdminChatToken } from './routes/ui/keys.js'; import llmRoutes from './routes/llm.js'; import { createMCPPostHandler, createMCPGetHandler, createMCPDeleteHandler } from './routes/mcp.js'; +import customServicesUiRoutes from './routes/ui/custom-services.js'; +import customProxyRoutes from './routes/custom-proxy.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const app = express(); @@ -119,6 +121,12 @@ app.use('/api/skill', (req, res, next) => { // UI routes - no API key needed (local admin access) app.use('/ui', uiRoutes); +// Custom services admin API (under UI auth) +app.use('/ui/custom-services', customServicesUiRoutes); + +// Custom service proxy (agent-facing, requires API key auth) +app.use('/api/custom', apiKeyAuth, readOnlyEnforce, customProxyRoutes); + // Webhook routes - no API key needed (uses signature verification instead) app.use('/webhooks', webhooksRoutes); From d89447a7d32ee9c5bdfd835d953211a70295a9e1 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Tue, 17 Feb 2026 20:40:32 -0700 Subject: [PATCH 03/41] feat(#249): add custom REST API endpoints backend foundation --- src/routes/readme.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/routes/readme.js b/src/routes/readme.js index 33644a33..413cba11 100644 --- a/src/routes/readme.js +++ b/src/routes/readme.js @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { getAccountsByService, getMessagingMode, checkServiceAccess } from '../lib/db.js'; +import { getAccountsByService, getMessagingMode, checkServiceAccess, listCustomServices, listCustomServiceAccounts } from '../lib/db.js'; import SERVICE_REGISTRY from '../lib/serviceRegistry.js'; const router = Router(); @@ -54,6 +54,25 @@ router.get('/', (req, res) => { urlPattern: '/api/{service}/{accountName}/...', services, endpoints, + customServices: (() => { + const customs = listCustomServices().filter(s => s.enabled); + if (customs.length === 0) return undefined; + const result = {}; + for (const svc of customs) { + const accounts = listCustomServiceAccounts(svc.name).filter(a => a.enabled).map(a => a.account_name); + if (accounts.length === 0) continue; + result[svc.name] = { + displayName: svc.display_name, + description: svc.description, + docs: svc.docs_url, + category: svc.category, + accounts, + base: `/api/custom/${svc.name}/{accountName}`, + endpoints: svc.endpoints.map(ep => `${ep.method} /api/custom/${svc.name}/{accountName}${ep.path}`) + }; + } + return Object.keys(result).length > 0 ? result : undefined; + })(), auth: { type: 'bearer', header: 'Authorization: Bearer {your_api_key}' From d7e9c33f0f316dc7aa9673a9146c234dbe6d0e44 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Tue, 17 Feb 2026 20:40:33 -0700 Subject: [PATCH 04/41] feat(#249): add custom REST API endpoints backend foundation --- src/services/customServiceService.js | 155 +++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 src/services/customServiceService.js diff --git a/src/services/customServiceService.js b/src/services/customServiceService.js new file mode 100644 index 00000000..b417b8f8 --- /dev/null +++ b/src/services/customServiceService.js @@ -0,0 +1,155 @@ +// Custom service business logic layer (#249) +import { + createCustomService as dbCreate, + getCustomService as dbGet, + listCustomServices as dbList, + updateCustomService as dbUpdate, + deleteCustomService as dbDelete, + createCustomServiceAccount as dbCreateAccount, + getCustomServiceAccount as dbGetAccount, + listCustomServiceAccounts as dbListAccounts, + updateCustomServiceAccount as dbUpdateAccount, + deleteCustomServiceAccount as dbDeleteAccount, + listEnabledCustomServices +} from '../lib/db.js'; + +// Validate service name: URL-safe, starts with letter +const NAME_RE = /^[a-z][a-z0-9_-]*$/; + +/** + * Create a custom service definition + */ +export function createCustomService(data) { + if (!data.name || !NAME_RE.test(data.name)) { + throw new Error('Service name must match ^[a-z][a-z0-9_-]*$'); + } + if (!data.baseUrl) { + throw new Error('Base URL is required'); + } + if (!data.displayName) { + throw new Error('Display name is required'); + } + if (!data.authConfig || !data.authConfig.type) { + throw new Error('Auth config with type is required'); + } + // Check for name collision with built-in services + const builtins = ['github', 'bluesky', 'reddit', 'calendar', 'mastodon', 'linkedin', 'youtube', 'jira', 'fitbit', 'brave', 'google_search', 'google_calendar']; + if (builtins.includes(data.name)) { + throw new Error(`Service name '${data.name}' conflicts with a built-in service`); + } + const existing = dbGet(data.name); + if (existing) { + throw new Error(`Custom service '${data.name}' already exists`); + } + return dbCreate(data); +} + +export function getCustomService(name) { + return dbGet(name); +} + +export function listCustomServices() { + return dbList(); +} + +export function updateCustomService(name, updates) { + const existing = dbGet(name); + if (!existing) { + throw new Error(`Custom service '${name}' not found`); + } + return dbUpdate(name, updates); +} + +export function deleteCustomService(name) { + return dbDelete(name); +} + +// Account CRUD +export function createAccount(serviceName, accountName, credentials) { + if (!accountName || !accountName.trim()) { + throw new Error('Account name is required'); + } + return dbCreateAccount(serviceName, accountName, credentials); +} + +export function getAccount(serviceName, accountName) { + return dbGetAccount(serviceName, accountName); +} + +export function listAccounts(serviceName) { + return dbListAccounts(serviceName); +} + +export function updateAccount(serviceName, accountName, updates) { + return dbUpdateAccount(serviceName, accountName, updates); +} + +export function deleteAccount(serviceName, accountName) { + return dbDeleteAccount(serviceName, accountName); +} + +/** + * Get all enabled custom services with their accounts (for route registration) + */ +export function getEnabledServices() { + return listEnabledCustomServices(); +} + +/** + * Test connection to a custom service by making a GET request to baseUrl + */ +export async function testConnection(serviceName, accountName) { + const service = dbGet(serviceName); + if (!service) throw new Error('Service not found'); + + let account = null; + if (accountName) { + account = dbGetAccount(serviceName, accountName); + if (!account) throw new Error('Account not found'); + } + + const url = service.base_url; + const headers = {}; + + // Inject auth if account provided + if (account) { + injectAuth(headers, {}, service.auth_config, account.credentials); + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10000); + try { + const resp = await fetch(url, { method: 'GET', headers, signal: controller.signal }); + return { ok: resp.ok, status: resp.status, statusText: resp.statusText }; + } catch (err) { + return { ok: false, status: 0, statusText: err.message }; + } finally { + clearTimeout(timeout); + } +} + +/** + * Inject auth credentials into headers/query based on auth config + */ +export function injectAuth(headers, query, authConfig, credentials) { + if (!authConfig || !credentials) return; + const type = authConfig.type; + + if (type === 'bearer') { + headers['Authorization'] = `Bearer ${credentials.token}`; + } else if (type === 'basic') { + const encoded = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64'); + headers['Authorization'] = `Basic ${encoded}`; + } else if (type === 'api-key') { + const injection = authConfig.injection || 'header'; + if (injection === 'header') { + headers[authConfig.headerName || 'X-API-Key'] = credentials.apiKey; + } else if (injection === 'query') { + query[authConfig.paramName || 'api_key'] = credentials.apiKey; + } + } else if (type === 'custom-header') { + headers[credentials.headerName || authConfig.headerName] = credentials.headerValue; + } else if (type === 'query-param') { + query[credentials.paramName || authConfig.paramName] = credentials.paramValue; + } +} From 188b692bb0c58e0603c362b0d2f9ee9cde8f2273 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Tue, 17 Feb 2026 20:40:33 -0700 Subject: [PATCH 05/41] feat(#249): add custom REST API endpoints backend foundation --- src/routes/ui/custom-services.js | 128 +++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/routes/ui/custom-services.js diff --git a/src/routes/ui/custom-services.js b/src/routes/ui/custom-services.js new file mode 100644 index 00000000..d767f41f --- /dev/null +++ b/src/routes/ui/custom-services.js @@ -0,0 +1,128 @@ +// Admin UI routes for custom services (#249) +import { Router } from 'express'; +import { + createCustomService, + getCustomService, + listCustomServices, + updateCustomService, + deleteCustomService, + createAccount, + getAccount, + listAccounts, + updateAccount, + deleteAccount, + testConnection +} from '../../services/customServiceService.js'; + +const router = Router(); + +// ---- Service CRUD (JSON API) ---- + +// List all custom services +router.get('/api', (req, res) => { + try { + const services = listCustomServices(); + res.json({ services }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// Get single service +router.get('/api/:name', (req, res) => { + try { + const service = getCustomService(req.params.name); + if (!service) return res.status(404).json({ error: 'Service not found' }); + const accounts = listAccounts(req.params.name); + res.json({ service, accounts }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// Create service +router.post('/api', (req, res) => { + try { + const result = createCustomService(req.body); + res.status(201).json(result); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +// Update service +router.put('/api/:name', (req, res) => { + try { + updateCustomService(req.params.name, req.body); + const updated = getCustomService(req.params.name); + res.json(updated); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +// Delete service +router.delete('/api/:name', (req, res) => { + try { + deleteCustomService(req.params.name); + res.json({ success: true }); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +// ---- Account CRUD ---- + +// List accounts for a service +router.get('/api/:name/accounts', (req, res) => { + try { + const accounts = listAccounts(req.params.name); + res.json({ accounts }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// Create account +router.post('/api/:name/accounts', (req, res) => { + try { + const { accountName, credentials } = req.body; + const result = createAccount(req.params.name, accountName, credentials || {}); + res.status(201).json(result); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +// Update account +router.put('/api/:name/accounts/:acct', (req, res) => { + try { + updateAccount(req.params.name, req.params.acct, req.body); + res.json({ success: true }); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +// Delete account +router.delete('/api/:name/accounts/:acct', (req, res) => { + try { + deleteAccount(req.params.name, req.params.acct); + res.json({ success: true }); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +// ---- Test connection ---- +router.post('/api/:name/test', async (req, res) => { + try { + const { accountName } = req.body; + const result = await testConnection(req.params.name, accountName); + res.json(result); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +export default router; From c9e0eeacb3646f3b2f7e09307e94b69a5eb5a6f6 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Tue, 17 Feb 2026 20:40:34 -0700 Subject: [PATCH 06/41] feat(#249): add custom REST API endpoints backend foundation --- src/routes/custom-proxy.js | 213 +++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 src/routes/custom-proxy.js diff --git a/src/routes/custom-proxy.js b/src/routes/custom-proxy.js new file mode 100644 index 00000000..dabdfb29 --- /dev/null +++ b/src/routes/custom-proxy.js @@ -0,0 +1,213 @@ +// Dynamic proxy for custom services (#249) +// Loads enabled custom services and proxies requests to upstream APIs +import { Router } from 'express'; +import { getEnabledServices, injectAuth } from '../services/customServiceService.js'; +import { getCustomServiceAccount, getCustomService } from '../lib/db.js'; + +const router = Router(); + +// Cache of loaded services (refreshed on CRUD operations) +let serviceCache = null; + +export function refreshCustomServiceCache() { + serviceCache = null; +} + +function loadServices() { + if (!serviceCache) { + serviceCache = getEnabledServices(); + } + return serviceCache; +} + +/** + * Match a request path against an endpoint definition path pattern + * e.g. /devices/{deviceId}/status matches /devices/abc123/status + * Returns extracted path params or null if no match + */ +function matchEndpointPath(pattern, requestPath) { + const patternParts = pattern.split('/').filter(Boolean); + const requestParts = requestPath.split('/').filter(Boolean); + if (patternParts.length !== requestParts.length) return null; + + const params = {}; + for (let i = 0; i < patternParts.length; i++) { + const pp = patternParts[i]; + if (pp.startsWith('{') && pp.endsWith('}')) { + params[pp.slice(1, -1)] = requestParts[i]; + } else if (pp !== requestParts[i]) { + return null; + } + } + return params; +} + +/** + * Simple JSON Schema validation (top-level properties only) + * Returns array of error strings, empty if valid + */ +function validateSchema(data, schema, prefix) { + const errors = []; + if (!schema || schema.type !== 'object' || !schema.properties) return errors; + + const required = schema.required || []; + for (const field of required) { + if (data[field] === undefined || data[field] === null) { + errors.push({ path: `${prefix}.${field}`, message: 'is required' }); + } + } + + for (const [key, propSchema] of Object.entries(schema.properties)) { + const val = data[key]; + if (val === undefined) continue; + + if (propSchema.type === 'string' && typeof val !== 'string') { + errors.push({ path: `${prefix}.${key}`, message: 'must be string' }); + } else if (propSchema.type === 'integer' && !Number.isInteger(Number(val))) { + errors.push({ path: `${prefix}.${key}`, message: 'must be integer' }); + } else if (propSchema.type === 'number' && isNaN(Number(val))) { + errors.push({ path: `${prefix}.${key}`, message: 'must be number' }); + } else if (propSchema.type === 'boolean' && typeof val !== 'boolean') { + errors.push({ path: `${prefix}.${key}`, message: 'must be boolean' }); + } + + if (propSchema.enum && !propSchema.enum.includes(val)) { + errors.push({ path: `${prefix}.${key}`, message: `must be one of: ${propSchema.enum.join(', ')}` }); + } + } + return errors; +} + +/** + * Build the upstream URL by substituting path params into the endpoint path + */ +function buildUpstreamUrl(baseUrl, endpointPath, pathParams, queryParams) { + let path = endpointPath; + for (const [key, val] of Object.entries(pathParams)) { + path = path.replace(`{${key}}`, encodeURIComponent(val)); + } + const url = new URL(path, baseUrl.endsWith('/') ? baseUrl : baseUrl + '/'); + if (queryParams) { + for (const [key, val] of Object.entries(queryParams)) { + if (val !== undefined && val !== null) { + url.searchParams.set(key, val); + } + } + } + return url.toString(); +} + +// Main handler: /api/custom/{serviceName}/{accountName}/... +router.all('/:serviceName/:accountName/*', async (req, res) => { + const { serviceName, accountName } = req.params; + const restPath = '/' + req.params[0]; // everything after accountName + + // Load service definition + const service = getCustomService(serviceName); + if (!service || !service.enabled) { + return res.status(404).json({ error: `Custom service '${serviceName}' not found or disabled` }); + } + + // Load account credentials + const account = getCustomServiceAccount(serviceName, accountName); + if (!account || !account.enabled) { + return res.status(404).json({ error: `Account '${accountName}' not found for service '${serviceName}'` }); + } + + // Find matching endpoint + const method = req.method.toUpperCase(); + let matchedEndpoint = null; + let pathParams = {}; + + for (const ep of service.endpoints) { + if (ep.method.toUpperCase() !== method) continue; + const params = matchEndpointPath(ep.path, restPath); + if (params) { + matchedEndpoint = ep; + pathParams = params; + break; + } + } + + if (!matchedEndpoint) { + return res.status(404).json({ + error: 'No matching endpoint found', + service: serviceName, + method, + path: restPath, + available: service.endpoints.map(e => `${e.method} ${e.path}`) + }); + } + + // Validate path params + if (matchedEndpoint.pathParams) { + const errors = validateSchema(pathParams, matchedEndpoint.pathParams, 'path'); + if (errors.length > 0) { + return res.status(400).json({ error: 'validation_failed', details: errors }); + } + } + + // Validate query params + if (matchedEndpoint.queryParams) { + const errors = validateSchema(req.query, matchedEndpoint.queryParams, 'query'); + if (errors.length > 0) { + return res.status(400).json({ error: 'validation_failed', details: errors }); + } + } + + // Validate body + if (matchedEndpoint.bodySchema && ['POST', 'PUT', 'PATCH'].includes(method)) { + const errors = validateSchema(req.body || {}, matchedEndpoint.bodySchema, 'body'); + if (errors.length > 0) { + return res.status(400).json({ error: 'validation_failed', details: errors }); + } + } + + // Build upstream URL + const query = { ...req.query }; + const headers = { 'Content-Type': 'application/json' }; + + // Inject auth + injectAuth(headers, query, service.auth_config, account.credentials); + + const upstreamUrl = buildUpstreamUrl(service.base_url, matchedEndpoint.path, pathParams, query); + + // Proxy request + try { + const fetchOptions = { method, headers }; + if (['POST', 'PUT', 'PATCH'].includes(method) && req.body) { + fetchOptions.body = JSON.stringify(req.body); + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 30000); + fetchOptions.signal = controller.signal; + + const upstream = await fetch(upstreamUrl, fetchOptions); + clearTimeout(timeout); + + const contentType = upstream.headers.get('content-type') || ''; + let body; + if (contentType.includes('application/json')) { + body = await upstream.json(); + } else { + body = await upstream.text(); + } + + res.status(upstream.status).json({ + _custom_service: serviceName, + _endpoint: matchedEndpoint.name, + status: upstream.status, + data: body + }); + } catch (err) { + res.status(502).json({ + error: 'upstream_error', + message: err.message, + service: serviceName, + endpoint: matchedEndpoint.name + }); + } +}); + +export default router; From d44288c58055b6f8334a891e987003c8eef4078b Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Wed, 18 Feb 2026 01:14:02 -0700 Subject: [PATCH 07/41] fix: address review feedback on PR #283 (#249) - Remove unused imports (getAccount, getEnabledServices) - Remove dead cache code (serviceCache, loadServices) - Mount custom-services under UI router with requireAuth - Register custom-proxy in index.js - Add custom services to /api/readme - Replace hardcoded builtins list with SERVICE_REGISTRY --- src/index.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/index.js b/src/index.js index 304d89e2..2b5ebd72 100644 --- a/src/index.js +++ b/src/index.js @@ -31,9 +31,8 @@ import { setupHumanChannelProxy, setAdminTokenValidator } from './routes/channel import { setupAgentChannelProxy } from './routes/channel-agent.js'; import { validateAdminChatToken } from './routes/ui/keys.js'; import llmRoutes from './routes/llm.js'; -import { createMCPPostHandler, createMCPGetHandler, createMCPDeleteHandler } from './routes/mcp.js'; -import customServicesUiRoutes from './routes/ui/custom-services.js'; import customProxyRoutes from './routes/custom-proxy.js'; +import { createMCPPostHandler, createMCPGetHandler, createMCPDeleteHandler } from './routes/mcp.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const app = express(); @@ -102,6 +101,9 @@ app.use('/api/agents/memento', apiKeyAuth, (req, res, next) => { // LLM proxy - require auth, no read-only enforcement (POST for completions) app.use('/api/llm', apiKeyAuth, llmRoutes); +// Custom service proxy - require auth and read-only enforcement +app.use('/api/custom', apiKeyAuth, readOnlyEnforce, customProxyRoutes); + // MCP server - Streamable HTTP transport (requires auth) // POST handles initialization + messages, GET opens optional SSE stream, DELETE terminates session app.post('/mcp', apiKeyAuth, createMCPPostHandler()); @@ -121,12 +123,6 @@ app.use('/api/skill', (req, res, next) => { // UI routes - no API key needed (local admin access) app.use('/ui', uiRoutes); -// Custom services admin API (under UI auth) -app.use('/ui/custom-services', customServicesUiRoutes); - -// Custom service proxy (agent-facing, requires API key auth) -app.use('/api/custom', apiKeyAuth, readOnlyEnforce, customProxyRoutes); - // Webhook routes - no API key needed (uses signature verification instead) app.use('/webhooks', webhooksRoutes); From fd6d9f39d37dc874c1141b9096879bc06fd35158 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Wed, 18 Feb 2026 01:14:02 -0700 Subject: [PATCH 08/41] fix: address review feedback on PR #283 (#249) - Remove unused imports (getAccount, getEnabledServices) - Remove dead cache code (serviceCache, loadServices) - Mount custom-services under UI router with requireAuth - Register custom-proxy in index.js - Add custom services to /api/readme - Replace hardcoded builtins list with SERVICE_REGISTRY --- src/routes/ui/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/routes/ui/index.js b/src/routes/ui/index.js index 93cd5d31..0ae4f632 100644 --- a/src/routes/ui/index.js +++ b/src/routes/ui/index.js @@ -14,6 +14,7 @@ import accessRouter from './access.js'; import llmRouter from './llm.js'; import serviceDetailRouter from './service-detail.js'; import webhooksRouter from './webhooks.js'; +import customServicesRouter from './custom-services.js'; // Create the main UI router const router = Router(); @@ -51,6 +52,9 @@ router.use('/services', serviceDetailRouter); // Webhook management: /webhooks, /webhooks/add, /webhooks/:id router.use('/', webhooksRouter); +// Custom REST API services: /custom-services, /custom-services/api/* +router.use('/custom-services', customServicesRouter); + // Settings page: /settings // Also handles POST routes: /hsync/*, /messaging/*, /queue/settings/* router.use('/', settingsRouter); From 7d9b63801a1426dc99ec51816dfc49923db61bfb Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Wed, 18 Feb 2026 01:14:02 -0700 Subject: [PATCH 09/41] fix: address review feedback on PR #283 (#249) - Remove unused imports (getAccount, getEnabledServices) - Remove dead cache code (serviceCache, loadServices) - Mount custom-services under UI router with requireAuth - Register custom-proxy in index.js - Add custom services to /api/readme - Replace hardcoded builtins list with SERVICE_REGISTRY --- src/routes/readme.js | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/routes/readme.js b/src/routes/readme.js index 413cba11..898d7350 100644 --- a/src/routes/readme.js +++ b/src/routes/readme.js @@ -1,6 +1,7 @@ import { Router } from 'express'; -import { getAccountsByService, getMessagingMode, checkServiceAccess, listCustomServices, listCustomServiceAccounts } from '../lib/db.js'; +import { getAccountsByService, getMessagingMode, checkServiceAccess } from '../lib/db.js'; import SERVICE_REGISTRY from '../lib/serviceRegistry.js'; +import { listCustomServices } from '../services/customServiceService.js'; const router = Router(); @@ -48,31 +49,29 @@ router.get('/', (req, res) => { } } + // Add custom services + try { + const customServices = listCustomServices(); + for (const svc of customServices) { + if (!svc.enabled) continue; + services[svc.name] = { + description: svc.description || svc.display_name, + docs: svc.docs_url || undefined, + category: svc.category || 'custom', + accounts: ['(see custom service accounts)'], + examples: (svc.endpoints || []).map(ep => `${ep.method} /api/custom/${svc.name}/{accountName}${ep.path}`) + }; + } + } catch { + // Custom services table may not exist yet + } + res.json({ name: 'agentgate', description: 'API gateway for personal data with human-in-the-loop write approval. Read requests (GET) execute immediately. Write requests (POST/PUT/DELETE) are queued for human approval before execution.', urlPattern: '/api/{service}/{accountName}/...', services, endpoints, - customServices: (() => { - const customs = listCustomServices().filter(s => s.enabled); - if (customs.length === 0) return undefined; - const result = {}; - for (const svc of customs) { - const accounts = listCustomServiceAccounts(svc.name).filter(a => a.enabled).map(a => a.account_name); - if (accounts.length === 0) continue; - result[svc.name] = { - displayName: svc.display_name, - description: svc.description, - docs: svc.docs_url, - category: svc.category, - accounts, - base: `/api/custom/${svc.name}/{accountName}`, - endpoints: svc.endpoints.map(ep => `${ep.method} /api/custom/${svc.name}/{accountName}${ep.path}`) - }; - } - return Object.keys(result).length > 0 ? result : undefined; - })(), auth: { type: 'bearer', header: 'Authorization: Bearer {your_api_key}' From 7e57a5f97e45e126a417343e5c6771f96df151b4 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Wed, 18 Feb 2026 01:14:03 -0700 Subject: [PATCH 10/41] fix: address review feedback on PR #283 (#249) - Remove unused imports (getAccount, getEnabledServices) - Remove dead cache code (serviceCache, loadServices) - Mount custom-services under UI router with requireAuth - Register custom-proxy in index.js - Add custom services to /api/readme - Replace hardcoded builtins list with SERVICE_REGISTRY --- src/routes/custom-proxy.js | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/routes/custom-proxy.js b/src/routes/custom-proxy.js index dabdfb29..c1012e71 100644 --- a/src/routes/custom-proxy.js +++ b/src/routes/custom-proxy.js @@ -1,25 +1,11 @@ // Dynamic proxy for custom services (#249) // Loads enabled custom services and proxies requests to upstream APIs import { Router } from 'express'; -import { getEnabledServices, injectAuth } from '../services/customServiceService.js'; +import { injectAuth } from '../services/customServiceService.js'; import { getCustomServiceAccount, getCustomService } from '../lib/db.js'; const router = Router(); -// Cache of loaded services (refreshed on CRUD operations) -let serviceCache = null; - -export function refreshCustomServiceCache() { - serviceCache = null; -} - -function loadServices() { - if (!serviceCache) { - serviceCache = getEnabledServices(); - } - return serviceCache; -} - /** * Match a request path against an endpoint definition path pattern * e.g. /devices/{deviceId}/status matches /devices/abc123/status From a4b2971e3f83528390af72cc3e0cac6674f7d724 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Wed, 18 Feb 2026 01:14:03 -0700 Subject: [PATCH 11/41] fix: address review feedback on PR #283 (#249) - Remove unused imports (getAccount, getEnabledServices) - Remove dead cache code (serviceCache, loadServices) - Mount custom-services under UI router with requireAuth - Register custom-proxy in index.js - Add custom services to /api/readme - Replace hardcoded builtins list with SERVICE_REGISTRY --- src/routes/ui/custom-services.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/ui/custom-services.js b/src/routes/ui/custom-services.js index d767f41f..c6045b5b 100644 --- a/src/routes/ui/custom-services.js +++ b/src/routes/ui/custom-services.js @@ -7,7 +7,6 @@ import { updateCustomService, deleteCustomService, createAccount, - getAccount, listAccounts, updateAccount, deleteAccount, From 4e407d9498032a75f7a17d3c5aab13de50673804 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Wed, 18 Feb 2026 01:14:04 -0700 Subject: [PATCH 12/41] fix: address review feedback on PR #283 (#249) - Remove unused imports (getAccount, getEnabledServices) - Remove dead cache code (serviceCache, loadServices) - Mount custom-services under UI router with requireAuth - Register custom-proxy in index.js - Add custom services to /api/readme - Replace hardcoded builtins list with SERVICE_REGISTRY --- src/services/customServiceService.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/services/customServiceService.js b/src/services/customServiceService.js index b417b8f8..fd831bfc 100644 --- a/src/services/customServiceService.js +++ b/src/services/customServiceService.js @@ -12,6 +12,7 @@ import { deleteCustomServiceAccount as dbDeleteAccount, listEnabledCustomServices } from '../lib/db.js'; +import SERVICE_REGISTRY from '../lib/serviceRegistry.js'; // Validate service name: URL-safe, starts with letter const NAME_RE = /^[a-z][a-z0-9_-]*$/; @@ -32,9 +33,9 @@ export function createCustomService(data) { if (!data.authConfig || !data.authConfig.type) { throw new Error('Auth config with type is required'); } - // Check for name collision with built-in services - const builtins = ['github', 'bluesky', 'reddit', 'calendar', 'mastodon', 'linkedin', 'youtube', 'jira', 'fitbit', 'brave', 'google_search', 'google_calendar']; - if (builtins.includes(data.name)) { + // Check for name collision with built-in services (from registry) + const builtinNames = Object.keys(SERVICE_REGISTRY); + if (builtinNames.includes(data.name)) { throw new Error(`Service name '${data.name}' conflicts with a built-in service`); } const existing = dbGet(data.name); From 7fdaeb953833014604f5197e0e30a0a3e01c3f42 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Wed, 18 Feb 2026 01:20:20 -0700 Subject: [PATCH 13/41] fix: add custom service mocks to ui.test.js Tests were failing because the db.js mock did not include the new custom service exports. --- tests/ui.test.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/ui.test.js b/tests/ui.test.js index cfc876ea..503d2b5f 100644 --- a/tests/ui.test.js +++ b/tests/ui.test.js @@ -164,7 +164,20 @@ jest.unstable_mockModule('../src/lib/db.js', () => ({ deleteMcpSessionsForAgent: jest.fn(() => ({ changes: 0 })), deleteStaleMcpSessions: jest.fn(() => ({ changes: 0 })), getMcpSessionCounts: jest.fn(() => ({ total: 0, byAgent: {} })), - getMcpSessionCount: jest.fn(() => 0) + getMcpSessionCount: jest.fn(() => 0), + + // Custom services + createCustomService: jest.fn(), + getCustomService: jest.fn(), + listCustomServices: jest.fn(() => []), + updateCustomService: jest.fn(), + deleteCustomService: jest.fn(), + createCustomServiceAccount: jest.fn(), + getCustomServiceAccount: jest.fn(), + listCustomServiceAccounts: jest.fn(() => []), + updateCustomServiceAccount: jest.fn(), + deleteCustomServiceAccount: jest.fn(), + listEnabledCustomServices: jest.fn(() => []) })); // Mock hsyncManager From a3f74068d20959c397abb8789026c8c9e4f73172 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sat, 21 Feb 2026 21:25:18 -0700 Subject: [PATCH 14/41] feat: add credential encryption at rest (AES-256-GCM) for custom service accounts --- src/lib/credentials.js | 59 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/lib/credentials.js diff --git a/src/lib/credentials.js b/src/lib/credentials.js new file mode 100644 index 00000000..bc1ac9d5 --- /dev/null +++ b/src/lib/credentials.js @@ -0,0 +1,59 @@ +// Credential encryption utilities for custom service accounts (#283) +import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto'; + +const ALGORITHM = 'aes-256-gcm'; +const KEY_LENGTH = 32; +const IV_LENGTH = 16; +const TAG_LENGTH = 16; +const SALT = 'agentgate-credentials-v1'; // Static salt; key uniqueness comes from the secret + +let _cachedKey = null; + +function getEncryptionKey() { + if (_cachedKey) return _cachedKey; + const secret = process.env.CREDENTIALS_SECRET || process.env.AGENTGATE_SECRET || 'agentgate-default-secret'; + _cachedKey = scryptSync(secret, SALT, KEY_LENGTH); + return _cachedKey; +} + +/** + * Encrypt a plaintext string. Returns a hex-encoded string: iv + tag + ciphertext. + */ +export function encrypt(plaintext) { + const key = getEncryptionKey(); + const iv = randomBytes(IV_LENGTH); + const cipher = createCipheriv(ALGORITHM, key, iv); + const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); + const tag = cipher.getAuthTag(); + // Pack as: iv (16) + tag (16) + ciphertext + return Buffer.concat([iv, tag, encrypted]).toString('hex'); +} + +/** + * Decrypt a hex-encoded string produced by encrypt(). + * Returns the original plaintext. + */ +export function decrypt(hexString) { + const key = getEncryptionKey(); + const data = Buffer.from(hexString, 'hex'); + const iv = data.subarray(0, IV_LENGTH); + const tag = data.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH); + const ciphertext = data.subarray(IV_LENGTH + TAG_LENGTH); + const decipher = createDecipheriv(ALGORITHM, key, iv); + decipher.setAuthTag(tag); + return decipher.update(ciphertext) + decipher.final('utf8'); +} + +/** + * Encrypt a credentials object (JSON-serializes then encrypts). + */ +export function encryptCredentials(credentials) { + return encrypt(JSON.stringify(credentials)); +} + +/** + * Decrypt a credentials string back to an object. + */ +export function decryptCredentials(encryptedHex) { + return JSON.parse(decrypt(encryptedHex)); +} From a0c37ff447c7b0a5299471f6f9bbfdd412a85057 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sat, 21 Feb 2026 21:25:18 -0700 Subject: [PATCH 15/41] feat: encrypt/decrypt credentials in custom service account CRUD --- src/lib/db.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lib/db.js b/src/lib/db.js index 677dd07c..c6b325e8 100644 --- a/src/lib/db.js +++ b/src/lib/db.js @@ -5,6 +5,7 @@ import { homedir } from 'os'; import { mkdirSync, existsSync, unlinkSync, readdirSync } from 'fs'; import bcrypt from 'bcrypt'; import { stemmer } from 'stemmer'; +import { encryptCredentials, decryptCredentials } from './credentials.js'; // Data directory: AGENTGATE_DATA_DIR env var, or ~/.agentgate/ const dataDir = process.env.AGENTGATE_DATA_DIR || join(homedir(), '.agentgate'); @@ -2549,7 +2550,7 @@ export function createCustomServiceAccount(serviceName, accountName, credentials const result = db.prepare(` INSERT INTO custom_service_accounts (service_id, account_name, credentials) VALUES (?, ?, ?) - `).run(svc.id, accountName, JSON.stringify(credentials)); + `).run(svc.id, accountName, encryptCredentials(credentials)); return { id: Number(result.lastInsertRowid), service: serviceName, account_name: accountName }; } @@ -2560,7 +2561,7 @@ export function getCustomServiceAccount(serviceName, accountName) { WHERE cs.name = ? AND csa.account_name = ? `).get(serviceName, accountName); if (!row) return null; - return { ...row, credentials: JSON.parse(row.credentials), enabled: row.enabled === 1 }; + return { ...row, credentials: decryptCredentials(row.credentials), enabled: row.enabled === 1 }; } export function listCustomServiceAccounts(serviceName) { @@ -2580,7 +2581,7 @@ export function updateCustomServiceAccount(serviceName, accountName, updates) { if (updates.credentials !== undefined) { db.prepare(` UPDATE custom_service_accounts SET credentials = ? WHERE service_id = ? AND account_name = ? - `).run(JSON.stringify(updates.credentials), svc.id, accountName); + `).run(encryptCredentials(updates.credentials), svc.id, accountName); } if (updates.enabled !== undefined) { db.prepare(` @@ -2618,7 +2619,7 @@ export function listEnabledCustomServices() { if (row.account_name) { services.get(row.name).accounts.push({ account_name: row.account_name, - credentials: JSON.parse(row.account_credentials) + credentials: decryptCredentials(row.account_credentials) }); } } From 60d6d75388d3ed57332c3561a30e351b3764f729 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sat, 21 Feb 2026 21:25:19 -0700 Subject: [PATCH 16/41] feat: add URL validation and SSRF protection for custom services --- src/services/customServiceService.js | 68 ++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/services/customServiceService.js b/src/services/customServiceService.js index fd831bfc..88e99194 100644 --- a/src/services/customServiceService.js +++ b/src/services/customServiceService.js @@ -1,4 +1,6 @@ // Custom service business logic layer (#249) +import { lookup as dnsLookup } from 'dns/promises'; + import { createCustomService as dbCreate, getCustomService as dbGet, @@ -17,6 +19,64 @@ import SERVICE_REGISTRY from '../lib/serviceRegistry.js'; // Validate service name: URL-safe, starts with letter const NAME_RE = /^[a-z][a-z0-9_-]*$/; +/** + * Validate that a base URL is a valid HTTP(S) URL. + */ +function validateBaseUrl(url) { + let parsed; + try { + parsed = new URL(url); + } catch { + throw new Error('Invalid base URL'); + } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + throw new Error('Base URL must use http or https protocol'); + } +} + +/** + * Check if an IP address is private/internal. + */ +function isPrivateIP(ip) { + // IPv4 private ranges + const parts = ip.split('.').map(Number); + if (parts.length === 4) { + if (parts[0] === 10) return true; + if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true; + if (parts[0] === 192 && parts[1] === 168) return true; + if (parts[0] === 127) return true; + if (parts[0] === 169 && parts[1] === 254) return true; + if (parts[0] === 0) return true; + } + // IPv6 + if (ip === '::1' || ip === '::') return true; + if (ip.startsWith('fc') || ip.startsWith('fd')) return true; // fc00::/7 + if (ip.startsWith('fe80')) return true; // link-local + return false; +} + +/** + * Resolve a URL's hostname and check it doesn't point to a private/internal IP (SSRF protection). + */ +async function assertNotPrivateUrl(url) { + const parsed = new URL(url); + const hostname = parsed.hostname; + // Check if hostname is already an IP + if (isPrivateIP(hostname)) { + throw new Error('Connections to private/internal addresses are not allowed'); + } + // Resolve DNS + try { + const result = await dnsLookup(hostname); + if (isPrivateIP(result.address)) { + throw new Error('Connections to private/internal addresses are not allowed'); + } + } catch (err) { + if (err.message.includes('private/internal')) throw err; + throw new Error(`DNS resolution failed for ${hostname}: ${err.message}`); + } +} + /** * Create a custom service definition */ @@ -27,6 +87,7 @@ export function createCustomService(data) { if (!data.baseUrl) { throw new Error('Base URL is required'); } + validateBaseUrl(data.baseUrl); if (!data.displayName) { throw new Error('Display name is required'); } @@ -58,6 +119,9 @@ export function updateCustomService(name, updates) { if (!existing) { throw new Error(`Custom service '${name}' not found`); } + if (updates.baseUrl) { + validateBaseUrl(updates.baseUrl); + } return dbUpdate(name, updates); } @@ -110,6 +174,10 @@ export async function testConnection(serviceName, accountName) { } const url = service.base_url; + + // SSRF protection: block requests to private/internal IPs + await assertNotPrivateUrl(url); + const headers = {}; // Inject auth if account provided From 274acc8d1764c403b141b0b01cf8bef606659de6 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sat, 21 Feb 2026 21:57:20 -0700 Subject: [PATCH 17/41] fix: SSRF proxy protection, DNS pinning, expanded private IP ranges, remove default secret (#283) --- src/lib/credentials.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/credentials.js b/src/lib/credentials.js index bc1ac9d5..fe422939 100644 --- a/src/lib/credentials.js +++ b/src/lib/credentials.js @@ -11,7 +11,10 @@ let _cachedKey = null; function getEncryptionKey() { if (_cachedKey) return _cachedKey; - const secret = process.env.CREDENTIALS_SECRET || process.env.AGENTGATE_SECRET || 'agentgate-default-secret'; + const secret = process.env.CREDENTIALS_SECRET || process.env.AGENTGATE_SECRET; + if (!secret) { + throw new Error('CREDENTIALS_SECRET or AGENTGATE_SECRET environment variable must be set to use credential encryption'); + } _cachedKey = scryptSync(secret, SALT, KEY_LENGTH); return _cachedKey; } From 4672fd95e9d540f28f26a403fbba0bba83118dee Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sat, 21 Feb 2026 21:57:21 -0700 Subject: [PATCH 18/41] fix: SSRF proxy protection, DNS pinning, expanded private IP ranges, remove default secret (#283) --- src/services/customServiceService.js | 49 ++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/services/customServiceService.js b/src/services/customServiceService.js index 88e99194..d02d2aa9 100644 --- a/src/services/customServiceService.js +++ b/src/services/customServiceService.js @@ -38,27 +38,43 @@ function validateBaseUrl(url) { * Check if an IP address is private/internal. */ function isPrivateIP(ip) { + // Handle IPv4-mapped IPv6 (::ffff:x.x.x.x) + if (ip.startsWith('::ffff:')) { + const mapped = ip.slice(7); + if (mapped.includes('.')) { + return isPrivateIP(mapped); + } + } + // IPv4 private ranges const parts = ip.split('.').map(Number); - if (parts.length === 4) { - if (parts[0] === 10) return true; - if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true; - if (parts[0] === 192 && parts[1] === 168) return true; - if (parts[0] === 127) return true; - if (parts[0] === 169 && parts[1] === 254) return true; - if (parts[0] === 0) return true; + if (parts.length === 4 && parts.every(p => p >= 0 && p <= 255)) { + if (parts[0] === 10) return true; // 10.0.0.0/8 + if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true; // 172.16.0.0/12 + if (parts[0] === 192 && parts[1] === 168) return true; // 192.168.0.0/16 + if (parts[0] === 127) return true; // 127.0.0.0/8 + if (parts[0] === 169 && parts[1] === 254) return true; // 169.254.0.0/16 link-local + if (parts[0] === 0) return true; // 0.0.0.0/8 + if (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127) return true; // 100.64.0.0/10 CGNAT + if (parts[0] === 198 && (parts[1] === 18 || parts[1] === 19)) return true; // 198.18.0.0/15 benchmarking + if (parts[0] >= 240) return true; // 240.0.0.0/4 reserved + if (parts[0] === 255 && parts[1] === 255 && parts[2] === 255 && parts[3] === 255) return true; // broadcast } + // IPv6 if (ip === '::1' || ip === '::') return true; - if (ip.startsWith('fc') || ip.startsWith('fd')) return true; // fc00::/7 - if (ip.startsWith('fe80')) return true; // link-local + if (ip.startsWith('fc') || ip.startsWith('fd')) return true; // fc00::/7 unique local + if (ip.startsWith('fe80')) return true; // fe80::/10 link-local + if (ip.startsWith('2001:db8')) return true; // 2001:db8::/32 documentation return false; } /** * Resolve a URL's hostname and check it doesn't point to a private/internal IP (SSRF protection). + * Returns { address, hostname } for DNS pinning — callers should use the resolved IP + * to construct the fetch URL and set the Host header to the original hostname. */ -async function assertNotPrivateUrl(url) { +export async function assertNotPrivateUrl(url) { const parsed = new URL(url); const hostname = parsed.hostname; // Check if hostname is already an IP @@ -71,6 +87,7 @@ async function assertNotPrivateUrl(url) { if (isPrivateIP(result.address)) { throw new Error('Connections to private/internal addresses are not allowed'); } + return { address: result.address, hostname }; } catch (err) { if (err.message.includes('private/internal')) throw err; throw new Error(`DNS resolution failed for ${hostname}: ${err.message}`); @@ -175,10 +192,14 @@ export async function testConnection(serviceName, accountName) { const url = service.base_url; - // SSRF protection: block requests to private/internal IPs - await assertNotPrivateUrl(url); + // SSRF protection with DNS pinning: resolve once, use the IP for fetch + const { address, hostname } = await assertNotPrivateUrl(url); + + // Build DNS-pinned URL: replace hostname with resolved IP + const parsed = new URL(url); + const pinnedUrl = `${parsed.protocol}//${address}${parsed.port ? ':' + parsed.port : ''}${parsed.pathname}${parsed.search}`; - const headers = {}; + const headers = { 'Host': hostname }; // Inject auth if account provided if (account) { @@ -188,7 +209,7 @@ export async function testConnection(serviceName, accountName) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); try { - const resp = await fetch(url, { method: 'GET', headers, signal: controller.signal }); + const resp = await fetch(pinnedUrl, { method: 'GET', headers, signal: controller.signal }); return { ok: resp.ok, status: resp.status, statusText: resp.statusText }; } catch (err) { return { ok: false, status: 0, statusText: err.message }; From 24a4f187d45400713495008a4bab2ce0e43a07d5 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sat, 21 Feb 2026 21:57:21 -0700 Subject: [PATCH 19/41] fix: SSRF proxy protection, DNS pinning, expanded private IP ranges, remove default secret (#283) --- src/routes/custom-proxy.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/routes/custom-proxy.js b/src/routes/custom-proxy.js index c1012e71..05f9b410 100644 --- a/src/routes/custom-proxy.js +++ b/src/routes/custom-proxy.js @@ -1,7 +1,7 @@ // Dynamic proxy for custom services (#249) // Loads enabled custom services and proxies requests to upstream APIs import { Router } from 'express'; -import { injectAuth } from '../services/customServiceService.js'; +import { injectAuth, assertNotPrivateUrl } from '../services/customServiceService.js'; import { getCustomServiceAccount, getCustomService } from '../lib/db.js'; const router = Router(); @@ -158,6 +158,17 @@ router.all('/:serviceName/:accountName/*', async (req, res) => { const upstreamUrl = buildUpstreamUrl(service.base_url, matchedEndpoint.path, pathParams, query); + // SSRF protection with DNS pinning: resolve once, use the IP for fetch + let pinnedUrl; + try { + const { address, hostname } = await assertNotPrivateUrl(upstreamUrl); + const parsed = new URL(upstreamUrl); + pinnedUrl = `${parsed.protocol}//${address}${parsed.port ? ':' + parsed.port : ''}${parsed.pathname}${parsed.search}`; + headers['Host'] = hostname; + } catch (err) { + return res.status(403).json({ error: 'ssrf_blocked', message: err.message }); + } + // Proxy request try { const fetchOptions = { method, headers }; @@ -169,7 +180,7 @@ router.all('/:serviceName/:accountName/*', async (req, res) => { const timeout = setTimeout(() => controller.abort(), 30000); fetchOptions.signal = controller.signal; - const upstream = await fetch(upstreamUrl, fetchOptions); + const upstream = await fetch(pinnedUrl, fetchOptions); clearTimeout(timeout); const contentType = upstream.headers.get('content-type') || ''; From 60c37d14428632be6d63f55077e56796c475a8cf Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sat, 21 Feb 2026 22:26:32 -0700 Subject: [PATCH 20/41] fix: skip DNS pinning for HTTPS (TLS compat), keep SSRF check --- src/services/customServiceService.js | 41 ++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/services/customServiceService.js b/src/services/customServiceService.js index d02d2aa9..9cef8bad 100644 --- a/src/services/customServiceService.js +++ b/src/services/customServiceService.js @@ -70,13 +70,20 @@ function isPrivateIP(ip) { } /** - * Resolve a URL's hostname and check it doesn't point to a private/internal IP (SSRF protection). - * Returns { address, hostname } for DNS pinning — callers should use the resolved IP - * to construct the fetch URL and set the Host header to the original hostname. + * Resolve a URL's hostname and verify it doesn't point to a private/internal IP (SSRF protection). + * + * For HTTP URLs: returns { address, hostname, pinnable: true } — callers can use the + * resolved IP directly to prevent DNS rebinding (TOCTOU). + * + * For HTTPS URLs: returns { address, hostname, pinnable: false } — DNS pinning would break + * TLS certificate validation (certs are issued for hostnames, not IPs), so callers should + * use the original URL. The DNS check still blocks private IPs at check time. */ export async function assertNotPrivateUrl(url) { const parsed = new URL(url); const hostname = parsed.hostname; + const isHttps = parsed.protocol === 'https:'; + // Check if hostname is already an IP if (isPrivateIP(hostname)) { throw new Error('Connections to private/internal addresses are not allowed'); @@ -87,13 +94,22 @@ export async function assertNotPrivateUrl(url) { if (isPrivateIP(result.address)) { throw new Error('Connections to private/internal addresses are not allowed'); } - return { address: result.address, hostname }; + return { address: result.address, hostname, pinnable: !isHttps }; } catch (err) { if (err.message.includes('private/internal')) throw err; throw new Error(`DNS resolution failed for ${hostname}: ${err.message}`); } } +/** + * Build a DNS-pinned URL by replacing the hostname with the resolved IP. + * Only use for HTTP (not HTTPS) — see assertNotPrivateUrl docs. + */ +export function buildPinnedUrl(originalUrl, resolvedIp) { + const parsed = new URL(originalUrl); + return `${parsed.protocol}//${resolvedIp}${parsed.port ? ':' + parsed.port : ''}${parsed.pathname}${parsed.search}`; +} + /** * Create a custom service definition */ @@ -192,14 +208,15 @@ export async function testConnection(serviceName, accountName) { const url = service.base_url; - // SSRF protection with DNS pinning: resolve once, use the IP for fetch - const { address, hostname } = await assertNotPrivateUrl(url); - - // Build DNS-pinned URL: replace hostname with resolved IP - const parsed = new URL(url); - const pinnedUrl = `${parsed.protocol}//${address}${parsed.port ? ':' + parsed.port : ''}${parsed.pathname}${parsed.search}`; + // SSRF protection: resolve DNS and check for private IPs + const { address, hostname, pinnable } = await assertNotPrivateUrl(url); - const headers = { 'Host': hostname }; + // For HTTP, use DNS-pinned URL to prevent rebinding; for HTTPS, use original URL + const fetchUrl = pinnable ? buildPinnedUrl(url, address) : url; + const headers = {}; + if (pinnable) { + headers['Host'] = hostname; + } // Inject auth if account provided if (account) { @@ -209,7 +226,7 @@ export async function testConnection(serviceName, accountName) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); try { - const resp = await fetch(pinnedUrl, { method: 'GET', headers, signal: controller.signal }); + const resp = await fetch(fetchUrl, { method: 'GET', headers, signal: controller.signal }); return { ok: resp.ok, status: resp.status, statusText: resp.statusText }; } catch (err) { return { ok: false, status: 0, statusText: err.message }; From 9b63b142775f0cdf9e9b52815dfbadbd496a7144 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sat, 21 Feb 2026 22:26:32 -0700 Subject: [PATCH 21/41] fix: skip DNS pinning for HTTPS (TLS compat), keep SSRF check --- src/routes/custom-proxy.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/routes/custom-proxy.js b/src/routes/custom-proxy.js index 05f9b410..9d61ed4d 100644 --- a/src/routes/custom-proxy.js +++ b/src/routes/custom-proxy.js @@ -1,7 +1,7 @@ // Dynamic proxy for custom services (#249) // Loads enabled custom services and proxies requests to upstream APIs import { Router } from 'express'; -import { injectAuth, assertNotPrivateUrl } from '../services/customServiceService.js'; +import { injectAuth, assertNotPrivateUrl, buildPinnedUrl } from '../services/customServiceService.js'; import { getCustomServiceAccount, getCustomService } from '../lib/db.js'; const router = Router(); @@ -158,13 +158,18 @@ router.all('/:serviceName/:accountName/*', async (req, res) => { const upstreamUrl = buildUpstreamUrl(service.base_url, matchedEndpoint.path, pathParams, query); - // SSRF protection with DNS pinning: resolve once, use the IP for fetch - let pinnedUrl; + // SSRF protection: resolve DNS and check for private IPs + // For HTTP: use DNS-pinned URL to prevent rebinding attacks + // For HTTPS: use original URL (pinning breaks TLS cert validation) + let fetchUrl; try { - const { address, hostname } = await assertNotPrivateUrl(upstreamUrl); - const parsed = new URL(upstreamUrl); - pinnedUrl = `${parsed.protocol}//${address}${parsed.port ? ':' + parsed.port : ''}${parsed.pathname}${parsed.search}`; - headers['Host'] = hostname; + const { address, hostname, pinnable } = await assertNotPrivateUrl(upstreamUrl); + if (pinnable) { + fetchUrl = buildPinnedUrl(upstreamUrl, address); + headers['Host'] = hostname; + } else { + fetchUrl = upstreamUrl; + } } catch (err) { return res.status(403).json({ error: 'ssrf_blocked', message: err.message }); } @@ -180,7 +185,7 @@ router.all('/:serviceName/:accountName/*', async (req, res) => { const timeout = setTimeout(() => controller.abort(), 30000); fetchOptions.signal = controller.signal; - const upstream = await fetch(pinnedUrl, fetchOptions); + const upstream = await fetch(fetchUrl, fetchOptions); clearTimeout(timeout); const contentType = upstream.headers.get('content-type') || ''; From 985824cd005589a87575103b6e462ecd033bad6f Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sat, 21 Feb 2026 23:26:14 -0700 Subject: [PATCH 22/41] fix: add @eslint/js to devDependencies (fixes CI lint) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index c87897b1..4b8797b9 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "ejs": "^3.1.10" }, "devDependencies": { + "@eslint/js": "^9.0.0", "eslint": "^9.0.0", "jest": "^29.7.0", "supertest": "^7.0.0" From 0c7c99e343516a5b981615e6ec46650a932936ae Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sat, 21 Feb 2026 23:26:15 -0700 Subject: [PATCH 23/41] fix: add @eslint/js to devDependencies (fixes CI lint) --- package-lock.json | 1108 +++++++++++++++------------------------------ 1 file changed, 371 insertions(+), 737 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5c63839e..ea1160f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "agentgate": "src/cli.js" }, "devDependencies": { + "@eslint/js": "^9.0.0", "eslint": "^9.0.0", "jest": "^29.7.0", "supertest": "^7.0.0" @@ -90,45 +91,10 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/core/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/generator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", - "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { @@ -159,26 +125,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -573,31 +519,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/traverse/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/types": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", @@ -690,31 +611,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/config-array/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@eslint/config-array/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", @@ -765,48 +661,34 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@eslint/eslintrc/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==", + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, - "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", "dev": true, "license": "MIT", "engines": { @@ -827,7 +709,7 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "1.0.0", + "version": "0.4.1", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, @@ -1416,22 +1298,6 @@ "node": ">= 0.6" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "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/@modelcontextprotocol/sdk/node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -1478,23 +1344,6 @@ "node": ">=6.6.0" } }, - "node_modules/@modelcontextprotocol/sdk/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/@modelcontextprotocol/sdk/node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -1630,12 +1479,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/@modelcontextprotocol/sdk/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/@modelcontextprotocol/sdk/node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -1645,21 +1488,6 @@ "node": ">= 0.6" } }, - "node_modules/@modelcontextprotocol/sdk/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/@modelcontextprotocol/sdk/node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -1743,9 +1571,9 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", "dev": true, "license": "MIT" }, @@ -1881,12 +1709,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", - "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/readable-stream": { @@ -1957,9 +1785,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -1980,16 +1808,15 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "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", @@ -2013,22 +1840,6 @@ } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "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/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2180,16 +1991,6 @@ "node": ">=8" } }, - "node_modules/babel-plugin-istanbul/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/babel-plugin-jest-hoist": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", @@ -2286,13 +2087,16 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bcrypt": { @@ -2330,14 +2134,15 @@ } }, "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", "license": "MIT", "dependencies": { - "buffer": "^5.5.0", + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", "inherits": "^2.0.4", - "readable-stream": "^3.4.0" + "readable-stream": "^4.2.0" } }, "node_modules/body-parser": { @@ -2364,6 +2169,36 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2445,11 +2280,11 @@ } }, "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { "type": "github", "url": "https://github.com/sponsors/feross" }, @@ -2465,7 +2300,7 @@ "license": "MIT", "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" } }, "node_modules/buffer-from": { @@ -2533,9 +2368,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001767", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", - "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", + "version": "1.0.30001772", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001772.tgz", + "integrity": "sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg==", "dev": true, "funding": [ { @@ -2728,6 +2563,20 @@ "typedarray": "^0.0.6" } }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -2778,18 +2627,12 @@ "node": ">= 0.8.0" } }, - "node_modules/cookie-parser/node_modules/cookie-signature": { + "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, "node_modules/cookiejar": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", @@ -2860,12 +2703,20 @@ } }, "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "ms": "2.0.0" + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/decompress-response": { @@ -3029,9 +2880,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", "dev": true, "license": "ISC" }, @@ -3102,29 +2953,6 @@ "node": ">=10.0.0" } }, - "node_modules/engine.io/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/engine.io/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/engine.io/node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -3232,9 +3060,9 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", "dependencies": { @@ -3244,7 +3072,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -3321,28 +3149,27 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/eslint/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/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==", + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, @@ -3601,15 +3428,21 @@ "express": ">= 4.11" } }, - "node_modules/express-rate-limit/node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "engines": { - "node": ">= 12" + "dependencies": { + "ms": "2.0.0" } }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3714,9 +3547,9 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.7.tgz", + "integrity": "sha512-FjiwU9HaHW6YB3H4a1sFudnv93lvydNjz2lmyUXR6IwKhGI+bgL3SOZrBGn6kvvX2pJvhEkGSGjyTHN47O4rqA==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -3756,6 +3589,21 @@ "node": ">= 0.8" } }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4094,9 +3942,9 @@ "license": "MIT" }, "node_modules/hono": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", - "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.1.tgz", + "integrity": "sha512-hi9afu8g0lfJVLolxElAZGANCTTl6bewIdsRNhaywfP9K8BPf++F2z6OLrYGIinUwpRKzbZHMhPwvc0ZEpAwGw==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -4125,53 +3973,6 @@ "node-datachannel": "^0.32.0" } }, - "node_modules/hsync/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/hsync/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/hsync/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/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4329,9 +4130,9 @@ "license": "MIT" }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", "license": "MIT", "engines": { "node": ">= 12" @@ -4480,6 +4281,19 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -4510,31 +4324,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-source-maps/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -4720,19 +4509,6 @@ } } }, - "node_modules/jest-config/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/jest-diff": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", @@ -5061,6 +4837,19 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -5236,8 +5025,8 @@ }, "node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "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": { @@ -5306,7 +5095,7 @@ } }, "node_modules/levn": { - "version": "1.0.0", + "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, @@ -5350,10 +5139,14 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } }, "node_modules/make-dir": { "version": "4.0.0", @@ -5371,6 +5164,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -5494,9 +5300,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", "dev": true, "license": "ISC", "dependencies": { @@ -5564,150 +5370,18 @@ "process-nextick-args": "^2.0.1" } }, - "node_modules/mqtt-packet/node_modules/bl": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", - "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", - "license": "MIT", - "dependencies": { - "@types/readable-stream": "^4.0.0", - "buffer": "^6.0.3", - "inherits": "^2.0.4", - "readable-stream": "^4.2.0" - } + "node_modules/mqtt/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" }, - "node_modules/mqtt-packet/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/mqtt-packet/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/mqtt-packet/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/mqtt-packet/node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/mqtt/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/mqtt/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/mqtt/node_modules/ms": { + "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/mqtt/node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/nanoid": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", @@ -5795,6 +5469,18 @@ "node": ">=10" } }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-addon-api": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", @@ -5813,9 +5499,9 @@ } }, "node_modules/node-datachannel": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/node-datachannel/-/node-datachannel-0.32.0.tgz", - "integrity": "sha512-kohrPL3rOWfiD+oJgc9R7Ibx10U3/yJ+oQ/uB2W2d2B+y4B1bkAMPoONRVS3PTz7AHFqdvoEe5VoRDZ+I6xFEw==", + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/node-datachannel/-/node-datachannel-0.32.1.tgz", + "integrity": "sha512-r4UdtA0lCsz6XrG84pJ6lntAyw/MHpmBOhEkg5UQcmWTEpANqCPkMos6rj/QZDdq3GBUsdI/wst5acwWUiibCA==", "hasInstallScript": true, "license": "MPL 2.0", "optional": true, @@ -5884,29 +5570,6 @@ "js-sdsl": "4.3.0" } }, - "node_modules/number-allocator/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/number-allocator/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/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6234,6 +5897,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", "license": "MIT", "dependencies": { "detect-libc": "^2.0.0", @@ -6398,18 +6062,34 @@ } }, "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "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.4.24", + "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" + } + }, + "node_modules/raw-body/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/rawr": { @@ -6436,6 +6116,15 @@ "rc": "cli.js" } }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -6444,17 +6133,19 @@ "license": "MIT" }, "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": ">= 6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/require-directory": { @@ -6562,29 +6253,6 @@ "node": ">= 18" } }, - "node_modules/router/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/router/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/router/node_modules/path-to-regexp": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", @@ -6622,15 +6290,13 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" } }, "node_modules/send": { @@ -6657,10 +6323,19 @@ "node": ">= 0.8.0" } }, - "node_modules/send/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==", + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/serve-static": { @@ -6902,29 +6577,6 @@ "ws": "~8.18.3" } }, - "node_modules/socket.io-adapter/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/socket.io-adapter/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/socket.io-adapter/node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -6959,52 +6611,6 @@ "node": ">=10.0.0" } }, - "node_modules/socket.io-parser/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/socket.io-parser/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/socket.io/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/socket.io/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/socks": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", @@ -7182,12 +6788,16 @@ } }, "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/superagent": { @@ -7211,24 +6821,6 @@ "node": ">=14.18.0" } }, - "node_modules/superagent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/superagent/node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -7242,13 +6834,6 @@ "node": ">=4.0.0" } }, - "node_modules/superagent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/supertest": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", @@ -7328,6 +6913,55 @@ "node": ">=6" } }, + "node_modules/tar-stream/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/tar-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/terminal-kit": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/terminal-kit/-/terminal-kit-3.1.2.tgz", @@ -7474,9 +7108,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, "node_modules/uniq": { @@ -7621,9 +7255,9 @@ } }, "node_modules/worker-timers": { - "version": "8.0.29", - "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.29.tgz", - "integrity": "sha512-9jk0MWHhWAZ2xlJPXr45oe5UF/opdpfZrY0HtyPizWuJ+ce1M3IYk/4IIdGct3kn9Ncfs+tkZt3w1tU6KW2Fsg==", + "version": "8.0.30", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.30.tgz", + "integrity": "sha512-8P7YoMHWN0Tz7mg+9oEhuZdjBIn2z6gfjlJqFcHiDd9no/oLnMGCARCDkV1LR3ccQus62ZdtIp7t3aTKrMLHOg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", From 795287b9a2802e3efb6388644ffe8822f5b339c2 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sat, 21 Feb 2026 23:53:04 -0700 Subject: [PATCH 24/41] chore: trigger CI --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 02375e3f..15e8261b 100644 --- a/README.md +++ b/README.md @@ -85,3 +85,4 @@ Configure your agent with the base URL and API key. Agents can use REST or MCP. ## License ISC + From 5a7347c3be18ad4aea689803e466a13ca71066cf Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Mon, 23 Feb 2026 21:26:48 -0700 Subject: [PATCH 25/41] fix: import readOnlyEnforce in index.js after merge with main --- src/index.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/index.js b/src/index.js index 2b5ebd72..8d381266 100644 --- a/src/index.js +++ b/src/index.js @@ -6,7 +6,7 @@ import { getCookieSecret } from './lib/db.js'; import { connectHsync } from './lib/hsyncManager.js'; import { startCloudflared } from './lib/cloudflareManager.js'; import { initSocket } from './lib/socketManager.js'; -import { apiKeyAuth, readOnlyEnforce, serviceAccessCheck } from './lib/middleware.js'; +import { apiKeyAuth, writeProxy, serviceAccessCheck, readOnlyEnforce } from './lib/middleware.js'; import githubRoutes from './routes/github.js'; import blueskyRoutes from './routes/bluesky.js'; import redditRoutes from './routes/reddit.js'; @@ -18,6 +18,7 @@ import jiraRoutes from './routes/jira.js'; import fitbitRoutes from './routes/fitbit.js'; import braveRoutes from './routes/brave.js'; import googleSearchRoutes from './routes/google-search.js'; +import homeassistantRoutes from './routes/homeassistant.js'; import queueRoutes from './routes/queue.js'; import agentsRoutes from './routes/agents.js'; import mementoRoutes from './routes/memento.js'; @@ -68,17 +69,18 @@ app.get('/health', (_req, res) => { // API routes - require auth, read-only, and service access check // Pattern: /api/{service}/{accountName}/... -app.use('/api/github', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('github'), githubRoutes); -app.use('/api/bluesky', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('bluesky'), blueskyRoutes); -app.use('/api/reddit', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('reddit'), redditRoutes); -app.use('/api/calendar', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('calendar'), calendarRoutes); -app.use('/api/mastodon', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('mastodon'), mastodonRoutes); -app.use('/api/linkedin', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('linkedin'), linkedinRoutes); -app.use('/api/youtube', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('youtube'), youtubeRoutes); -app.use('/api/jira', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('jira'), jiraRoutes); -app.use('/api/fitbit', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('fitbit'), fitbitRoutes); -app.use('/api/brave', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('brave'), braveRoutes); -app.use('/api/google_search', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('google_search'), googleSearchRoutes); +app.use('/api/github', apiKeyAuth, serviceAccessCheck('github'), writeProxy('github'), githubRoutes); +app.use('/api/bluesky', apiKeyAuth, serviceAccessCheck('bluesky'), writeProxy('bluesky'), blueskyRoutes); +app.use('/api/reddit', apiKeyAuth, serviceAccessCheck('reddit'), writeProxy('reddit'), redditRoutes); +app.use('/api/calendar', apiKeyAuth, serviceAccessCheck('calendar'), writeProxy('calendar'), calendarRoutes); +app.use('/api/mastodon', apiKeyAuth, serviceAccessCheck('mastodon'), writeProxy('mastodon'), mastodonRoutes); +app.use('/api/linkedin', apiKeyAuth, serviceAccessCheck('linkedin'), writeProxy('linkedin'), linkedinRoutes); +app.use('/api/youtube', apiKeyAuth, serviceAccessCheck('youtube'), writeProxy('youtube'), youtubeRoutes); +app.use('/api/jira', apiKeyAuth, serviceAccessCheck('jira'), writeProxy('jira'), jiraRoutes); +app.use('/api/fitbit', apiKeyAuth, serviceAccessCheck('fitbit'), writeProxy('fitbit'), fitbitRoutes); +app.use('/api/brave', apiKeyAuth, serviceAccessCheck('brave'), writeProxy('brave'), braveRoutes); +app.use('/api/google_search', apiKeyAuth, serviceAccessCheck('google_search'), writeProxy('google_search'), googleSearchRoutes); +app.use('/api/homeassistant', apiKeyAuth, serviceAccessCheck('homeassistant'), writeProxy('homeassistant'), homeassistantRoutes); // Service access management - admin API (requires auth) app.use('/api/services', apiKeyAuth, servicesRoutes); From c25e3809f5bd90ff4f5bc38a687223f5a3c20ab3 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Mon, 23 Feb 2026 21:27:30 -0700 Subject: [PATCH 26/41] fix: sync package.json with main (resolve merge conflict) --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4b8797b9..56bc0b9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agentgate", - "version": "0.12.2", + "version": "0.15.0", "type": "module", "description": "API gateway for AI agents with human-in-the-loop write approval", "main": "src/index.js", @@ -12,7 +12,8 @@ }, "files": [ "src/", - "public/" + "public/", + "views/" ], "repository": { "type": "git", From 8d748291fa6d69115abc863dbee0836899aa9625 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Mon, 23 Feb 2026 21:27:35 -0700 Subject: [PATCH 27/41] fix: sync package-lock.json with main (resolve merge conflict) --- package-lock.json | 1129 ++++++++++++++++++++++++++++++--------------- 1 file changed, 748 insertions(+), 381 deletions(-) diff --git a/package-lock.json b/package-lock.json index ea1160f1..f658badc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agentgate", - "version": "0.12.2", + "version": "0.15.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agentgate", - "version": "0.12.2", + "version": "0.15.0", "license": "ISC", "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", @@ -91,10 +91,45 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", + "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -125,6 +160,26 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -519,6 +574,31 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/types": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", @@ -611,6 +691,31 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", @@ -661,34 +766,48 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "ms": "^2.1.3" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { - "version": "9.39.3", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", - "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { @@ -709,7 +828,7 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", + "version": "1.0.0", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, @@ -1298,6 +1417,22 @@ "node": ">= 0.6" } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "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/@modelcontextprotocol/sdk/node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -1344,6 +1479,23 @@ "node": ">=6.6.0" } }, + "node_modules/@modelcontextprotocol/sdk/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/@modelcontextprotocol/sdk/node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -1479,6 +1631,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/@modelcontextprotocol/sdk/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/@modelcontextprotocol/sdk/node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -1488,6 +1646,21 @@ "node": ">= 0.6" } }, + "node_modules/@modelcontextprotocol/sdk/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/@modelcontextprotocol/sdk/node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -1571,9 +1744,9 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true, "license": "MIT" }, @@ -1709,12 +1882,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", - "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", + "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/readable-stream": { @@ -1785,9 +1958,9 @@ } }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -1808,15 +1981,16 @@ } }, "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "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" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, "funding": { "type": "github", @@ -1840,6 +2014,22 @@ } } }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "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/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1991,6 +2181,16 @@ "node": ">=8" } }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/babel-plugin-jest-hoist": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", @@ -2087,16 +2287,13 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" + "baseline-browser-mapping": "dist/cli.js" } }, "node_modules/bcrypt": { @@ -2134,15 +2331,14 @@ } }, "node_modules/bl": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", - "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "license": "MIT", "dependencies": { - "@types/readable-stream": "^4.0.0", - "buffer": "^6.0.3", + "buffer": "^5.5.0", "inherits": "^2.0.4", - "readable-stream": "^4.2.0" + "readable-stream": "^3.4.0" } }, "node_modules/body-parser": { @@ -2169,45 +2365,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/body-parser/node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { @@ -2280,9 +2446,9 @@ } }, "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -2300,7 +2466,7 @@ "license": "MIT", "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.2.1" + "ieee754": "^1.1.13" } }, "node_modules/buffer-from": { @@ -2368,9 +2534,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001772", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001772.tgz", - "integrity": "sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg==", + "version": "1.0.30001767", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", + "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", "dev": true, "funding": [ { @@ -2563,20 +2729,6 @@ "typedarray": "^0.0.6" } }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -2627,12 +2779,18 @@ "node": ">= 0.8.0" } }, - "node_modules/cookie-signature": { + "node_modules/cookie-parser/node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, "node_modules/cookiejar": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", @@ -2703,20 +2861,12 @@ } }, "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "ms": "2.0.0" } }, "node_modules/decompress-response": { @@ -2880,9 +3030,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.302", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", - "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "dev": true, "license": "ISC" }, @@ -2953,6 +3103,29 @@ "node": ">=10.0.0" } }, + "node_modules/engine.io/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/engine.io/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/engine.io/node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -3060,9 +3233,9 @@ } }, "node_modules/eslint": { - "version": "9.39.3", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", - "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", "dependencies": { @@ -3072,7 +3245,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.3", + "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -3149,27 +3322,28 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "node_modules/eslint/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "ms": "^2.1.3" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, @@ -3428,21 +3602,15 @@ "express": ">= 4.11" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/express-rate-limit/node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">= 12" } }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3547,9 +3715,9 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.7.tgz", - "integrity": "sha512-FjiwU9HaHW6YB3H4a1sFudnv93lvydNjz2lmyUXR6IwKhGI+bgL3SOZrBGn6kvvX2pJvhEkGSGjyTHN47O4rqA==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -3589,21 +3757,6 @@ "node": ">= 0.8" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3942,9 +4095,9 @@ "license": "MIT" }, "node_modules/hono": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.1.tgz", - "integrity": "sha512-hi9afu8g0lfJVLolxElAZGANCTTl6bewIdsRNhaywfP9K8BPf++F2z6OLrYGIinUwpRKzbZHMhPwvc0ZEpAwGw==", + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", + "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -3973,6 +4126,53 @@ "node-datachannel": "^0.32.0" } }, + "node_modules/hsync/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/hsync/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/hsync/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/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4130,9 +4330,9 @@ "license": "MIT" }, "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "license": "MIT", "engines": { "node": ">= 12" @@ -4281,19 +4481,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -4324,6 +4511,31 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -4509,6 +4721,19 @@ } } }, + "node_modules/jest-config/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-diff": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", @@ -4837,19 +5062,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -5025,8 +5237,8 @@ }, "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==", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "license": "MIT" }, "node_modules/json-schema-typed": { @@ -5095,7 +5307,7 @@ } }, "node_modules/levn": { - "version": "0.4.1", + "version": "1.0.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, @@ -5139,14 +5351,10 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" }, "node_modules/make-dir": { "version": "4.0.0", @@ -5164,23 +5372,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5300,9 +5495,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -5370,18 +5565,150 @@ "process-nextick-args": "^2.0.1" } }, - "node_modules/mqtt/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" + "node_modules/mqtt-packet/node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } }, - "node_modules/ms": { + "node_modules/mqtt-packet/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/mqtt-packet/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/mqtt-packet/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/mqtt-packet/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/mqtt/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/mqtt/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/mqtt/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/mqtt/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", @@ -5469,18 +5796,6 @@ "node": ">=10" } }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-addon-api": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", @@ -5499,9 +5814,9 @@ } }, "node_modules/node-datachannel": { - "version": "0.32.1", - "resolved": "https://registry.npmjs.org/node-datachannel/-/node-datachannel-0.32.1.tgz", - "integrity": "sha512-r4UdtA0lCsz6XrG84pJ6lntAyw/MHpmBOhEkg5UQcmWTEpANqCPkMos6rj/QZDdq3GBUsdI/wst5acwWUiibCA==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/node-datachannel/-/node-datachannel-0.32.0.tgz", + "integrity": "sha512-kohrPL3rOWfiD+oJgc9R7Ibx10U3/yJ+oQ/uB2W2d2B+y4B1bkAMPoONRVS3PTz7AHFqdvoEe5VoRDZ+I6xFEw==", "hasInstallScript": true, "license": "MPL 2.0", "optional": true, @@ -5570,6 +5885,29 @@ "js-sdsl": "4.3.0" } }, + "node_modules/number-allocator/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/number-allocator/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/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5897,7 +6235,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", "license": "MIT", "dependencies": { "detect-libc": "^2.0.0", @@ -6062,34 +6399,18 @@ } }, "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==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", + "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.10" - } - }, - "node_modules/raw-body/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": ">= 0.8" } }, "node_modules/rawr": { @@ -6116,15 +6437,6 @@ "rc": "cli.js" } }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -6133,19 +6445,17 @@ "license": "MIT" }, "node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">= 6" } }, "node_modules/require-directory": { @@ -6253,6 +6563,29 @@ "node": ">= 18" } }, + "node_modules/router/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/router/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/router/node_modules/path-to-regexp": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", @@ -6290,13 +6623,15 @@ "license": "MIT" }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/send": { @@ -6323,19 +6658,10 @@ "node": ">= 0.8.0" } }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/send/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/serve-static": { @@ -6577,6 +6903,29 @@ "ws": "~8.18.3" } }, + "node_modules/socket.io-adapter/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/socket.io-adapter/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/socket.io-adapter/node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -6611,6 +6960,52 @@ "node": ">=10.0.0" } }, + "node_modules/socket.io-parser/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/socket.io-parser/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/socket.io/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/socket.io/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/socks": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", @@ -6788,16 +7183,12 @@ } }, "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "license": "MIT", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, "node_modules/superagent": { @@ -6821,6 +7212,24 @@ "node": ">=14.18.0" } }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/superagent/node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -6834,6 +7243,13 @@ "node": ">=4.0.0" } }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/supertest": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", @@ -6913,55 +7329,6 @@ "node": ">=6" } }, - "node_modules/tar-stream/node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/tar-stream/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/terminal-kit": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/terminal-kit/-/terminal-kit-3.1.2.tgz", @@ -7108,9 +7475,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" }, "node_modules/uniq": { @@ -7255,9 +7622,9 @@ } }, "node_modules/worker-timers": { - "version": "8.0.30", - "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.30.tgz", - "integrity": "sha512-8P7YoMHWN0Tz7mg+9oEhuZdjBIn2z6gfjlJqFcHiDd9no/oLnMGCARCDkV1LR3ccQus62ZdtIp7t3aTKrMLHOg==", + "version": "8.0.29", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.29.tgz", + "integrity": "sha512-9jk0MWHhWAZ2xlJPXr45oe5UF/opdpfZrY0HtyPizWuJ+ce1M3IYk/4IIdGct3kn9Ncfs+tkZt3w1tU6KW2Fsg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", From 77520233eddba3d6a03a8069508781b5fde7898a Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Thu, 5 Mar 2026 21:30:47 -0700 Subject: [PATCH 28/41] chore: trigger CI on latest commit --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 56bc0b9e..2dca5fb2 100644 --- a/package.json +++ b/package.json @@ -68,3 +68,4 @@ "supertest": "^7.0.0" } } + From 9c9c9dc8497f8675a077b4f1a3d34a7f9552e909 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sun, 8 Mar 2026 21:08:20 -0700 Subject: [PATCH 29/41] =?UTF-8?q?fix:=20sync=20with=20main=20=E2=80=94=20r?= =?UTF-8?q?esolve=20merge=20conflicts,=20add=20sidecarSecret/rateLimit=20i?= =?UTF-8?q?mports=20(PR=20#283)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 8d381266..72df881d 100644 --- a/src/index.js +++ b/src/index.js @@ -6,7 +6,8 @@ import { getCookieSecret } from './lib/db.js'; import { connectHsync } from './lib/hsyncManager.js'; import { startCloudflared } from './lib/cloudflareManager.js'; import { initSocket } from './lib/socketManager.js'; -import { apiKeyAuth, writeProxy, serviceAccessCheck, readOnlyEnforce } from './lib/middleware.js'; +import { apiKeyAuth, writeProxy, serviceAccessCheck, sidecarSecretCheck, readOnlyEnforce } from './lib/middleware.js'; +import { globalRateLimit } from './lib/rateLimiter.js'; import githubRoutes from './routes/github.js'; import blueskyRoutes from './routes/bluesky.js'; import redditRoutes from './routes/reddit.js'; @@ -44,6 +45,9 @@ const PORT = process.env.PORT || 3050; // No API key auth — the target gateway handles its own authentication app.use('/px/:proxyId', createProxyRouter()); +// Global rate limit — 200 req/min per IP +app.use(globalRateLimit); + app.use(express.json({ limit: '10mb', verify: (req, res, buf) => { @@ -67,6 +71,9 @@ app.get('/health', (_req, res) => { res.json({ status: 'ok', timestamp: Date.now() }); }); +// Sidecar secret check — applied to ALL /api/* routes before any other middleware +app.use('/api', sidecarSecretCheck); + // API routes - require auth, read-only, and service access check // Pattern: /api/{service}/{accountName}/... app.use('/api/github', apiKeyAuth, serviceAccessCheck('github'), writeProxy('github'), githubRoutes); From a28da9fdf1df1612f0c55629c298c4610a3a2df8 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sun, 8 Mar 2026 21:08:21 -0700 Subject: [PATCH 30/41] =?UTF-8?q?fix:=20sync=20with=20main=20=E2=80=94=20r?= =?UTF-8?q?esolve=20merge=20conflicts,=20add=20sidecarSecret/rateLimit=20i?= =?UTF-8?q?mports=20(PR=20#283)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/ui.test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/ui.test.js b/tests/ui.test.js index 503d2b5f..3acc7d3a 100644 --- a/tests/ui.test.js +++ b/tests/ui.test.js @@ -48,6 +48,7 @@ jest.unstable_mockModule('../src/lib/db.js', () => ({ listUnnotifiedEntries: jest.fn(() => []), deleteQueueEntry: jest.fn(), clearQueueByStatus: jest.fn(), + clearAutoApprovedEntries: jest.fn(), clearCompletedQueue: jest.fn(), getPendingQueueCount: jest.fn(() => 0), getQueueCounts: jest.fn(() => ({ pending: 0, approved: 0, rejected: 0, completed: 0 })), @@ -100,6 +101,11 @@ jest.unstable_mockModule('../src/lib/db.js', () => ({ deleteBroadcast: jest.fn(), getBroadcast: jest.fn(), + // Sidecar Secret + setSidecarSecret: jest.fn(), + getSidecarSecretHash: jest.fn(() => null), + clearSidecarSecret: jest.fn(), + // Queue visibility getSharedQueueVisibility: jest.fn(() => false), setSharedQueueVisibility: jest.fn(), From 3d9ca973f59f8212d7ff9bb4400625851b79bf9f Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sun, 8 Mar 2026 21:08:21 -0700 Subject: [PATCH 31/41] =?UTF-8?q?fix:=20sync=20with=20main=20=E2=80=94=20r?= =?UTF-8?q?esolve=20merge=20conflicts,=20add=20sidecarSecret/rateLimit=20i?= =?UTF-8?q?mports=20(PR=20#283)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2dca5fb2..8cf20860 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agentgate", - "version": "0.15.0", + "version": "0.16.0", "type": "module", "description": "API gateway for AI agents with human-in-the-loop write approval", "main": "src/index.js", From 0b743c26bb60004393f1ef1dac9a6c2e1b8f6950 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sun, 8 Mar 2026 21:08:22 -0700 Subject: [PATCH 32/41] =?UTF-8?q?fix:=20sync=20with=20main=20=E2=80=94=20r?= =?UTF-8?q?esolve=20merge=20conflicts,=20add=20sidecarSecret/rateLimit=20i?= =?UTF-8?q?mports=20(PR=20#283)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/db.js | 115 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 97 insertions(+), 18 deletions(-) diff --git a/src/lib/db.js b/src/lib/db.js index c6b325e8..eadfddff 100644 --- a/src/lib/db.js +++ b/src/lib/db.js @@ -1004,6 +1004,32 @@ export function hasAdminPassword() { return getSetting('admin_password') !== null; } +// Sidecar Secret (cached — invalidated on set/clear) +let _cachedSidecarHash = undefined; // undefined = not yet loaded + +export async function setSidecarSecret(plaintext) { + const hash = await bcrypt.hash(plaintext, 10); + setSetting('sidecar_secret', hash); + _cachedSidecarHash = hash; +} + +export function getSidecarSecretHash() { + if (_cachedSidecarHash === undefined) { + _cachedSidecarHash = getSetting('sidecar_secret') || null; + } + return _cachedSidecarHash; +} + +export function clearSidecarSecret() { + deleteSetting('sidecar_secret'); + _cachedSidecarHash = null; +} + +// Exported for testing only +export function _resetSidecarCache() { + _cachedSidecarHash = undefined; +} + // Cookie secret (generated once, persisted) export function getCookieSecret() { let secret = getSetting('cookie_secret'); @@ -1039,31 +1065,73 @@ export function getQueueEntry(id) { }; } -export function listQueueEntries(status) { - let rows; - if (status) { - rows = db.prepare('SELECT * FROM write_queue WHERE status = ? ORDER BY submitted_at DESC').all(status); - } else { - rows = db.prepare('SELECT * FROM write_queue ORDER BY submitted_at DESC').all(); - } - return rows.map(row => ({ +const QUEUE_LIGHT_COLS = 'id, service, account_name, comment, status, rejection_reason, submitted_by, submitted_at, reviewed_at, completed_at, notified, notified_at, notify_error, auto_approved, reaction_emoji, requests'; + +function mapQueueRow(row) { + return { ...row, requests: JSON.parse(row.requests), results: row.results ? JSON.parse(row.results) : null, notified: Boolean(row.notified), auto_approved: Boolean(row.auto_approved) - })); + }; } -export function listAutoApprovedEntries() { - const rows = db.prepare('SELECT * FROM write_queue WHERE auto_approved = 1 ORDER BY submitted_at DESC').all(); - return rows.map(row => ({ - ...row, - requests: JSON.parse(row.requests), - results: row.results ? JSON.parse(row.results) : null, +function mapQueueRowLight(row) { + const requests = JSON.parse(row.requests); + return { + id: row.id, + service: row.service, + account_name: row.account_name, + comment: row.comment, + status: row.status, + rejection_reason: row.rejection_reason, + submitted_by: row.submitted_by, + submitted_at: row.submitted_at, + reviewed_at: row.reviewed_at, + completed_at: row.completed_at, notified: Boolean(row.notified), - auto_approved: true - })); + notified_at: row.notified_at, + notify_error: row.notify_error, + auto_approved: Boolean(row.auto_approved), + reaction_emoji: row.reaction_emoji, + requestSummary: requests.map(r => ({ method: r.method, path: r.path })), + resultCount: row.result_count || 0 + }; +} + +export function listQueueEntries(status, { limit, offset, light } = {}) { + const cols = light + ? QUEUE_LIGHT_COLS + ', (CASE WHEN results IS NOT NULL THEN json_array_length(results) ELSE 0 END) as result_count' + : '*'; + let rows; + if (status) { + if (limit !== null && limit !== undefined) { + rows = db.prepare(`SELECT ${cols} FROM write_queue WHERE status = ? ORDER BY submitted_at DESC LIMIT ? OFFSET ?`).all(status, limit, offset || 0); + } else { + rows = db.prepare(`SELECT ${cols} FROM write_queue WHERE status = ? ORDER BY submitted_at DESC`).all(status); + } + } else { + if (limit !== null && limit !== undefined) { + rows = db.prepare(`SELECT ${cols} FROM write_queue ORDER BY submitted_at DESC LIMIT ? OFFSET ?`).all(limit, offset || 0); + } else { + rows = db.prepare(`SELECT ${cols} FROM write_queue ORDER BY submitted_at DESC`).all(); + } + } + return rows.map(light ? mapQueueRowLight : mapQueueRow); +} + +export function listAutoApprovedEntries({ limit, offset, light } = {}) { + const cols = light + ? QUEUE_LIGHT_COLS + ', (CASE WHEN results IS NOT NULL THEN json_array_length(results) ELSE 0 END) as result_count' + : '*'; + let rows; + if (limit !== null && limit !== undefined) { + rows = db.prepare(`SELECT ${cols} FROM write_queue WHERE auto_approved = 1 ORDER BY submitted_at DESC LIMIT ? OFFSET ?`).all(limit, offset || 0); + } else { + rows = db.prepare(`SELECT ${cols} FROM write_queue WHERE auto_approved = 1 ORDER BY submitted_at DESC`).all(); + } + return rows.map(light ? mapQueueRowLight : mapQueueRow); } export function getAutoApprovedCount() { @@ -1071,6 +1139,10 @@ export function getAutoApprovedCount() { return row.count; } +export function clearAutoApprovedEntries() { + return db.prepare('DELETE FROM write_queue WHERE auto_approved = 1').run(); +} + export function updateQueueNotification(id, success, error = null) { if (success) { db.prepare(` @@ -1274,10 +1346,17 @@ export function rejectAgentMessage(id, reason) { } // Admin: list all messages (for UI) -export function listAgentMessages(status = null) { +export function listAgentMessages(status = null, { limit, offset } = {}) { + const hasLimit = limit !== null && limit !== undefined; if (status) { + if (hasLimit) { + return db.prepare('SELECT * FROM agent_messages WHERE status = ? ORDER BY created_at DESC LIMIT ? OFFSET ?').all(status, limit, offset || 0); + } return db.prepare('SELECT * FROM agent_messages WHERE status = ? ORDER BY created_at DESC').all(status); } + if (hasLimit) { + return db.prepare('SELECT * FROM agent_messages ORDER BY created_at DESC LIMIT ? OFFSET ?').all(limit, offset || 0); + } return db.prepare('SELECT * FROM agent_messages ORDER BY created_at DESC').all(); } From c03080841939885757b46b36f38c4c399f59dcb4 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sun, 8 Mar 2026 21:12:53 -0700 Subject: [PATCH 33/41] fix: sanitize upstream error messages, add design decision comments (PR #283 review) --- src/routes/custom-proxy.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/routes/custom-proxy.js b/src/routes/custom-proxy.js index 9d61ed4d..20ff7c2f 100644 --- a/src/routes/custom-proxy.js +++ b/src/routes/custom-proxy.js @@ -83,6 +83,18 @@ function buildUpstreamUrl(baseUrl, endpointPath, pathParams, queryParams) { return url.toString(); } +// Design decisions for custom proxy routes: +// +// 1. No writeProxy / approval queue: Custom services intentionally bypass the write +// approval queue used by built-in services. POST/PUT/DELETE go directly upstream. +// Rationale: custom services are explicitly configured by the admin, who controls +// which endpoints are exposed and to whom. The admin takes responsibility for the +// operations they define. readOnlyEnforce is still respected as a global safety net. +// +// 2. No serviceAccessCheck: Built-in service access checks are not applied here. +// Custom services are user-defined, not built-in — they have their own access +// control via account-level enabled/disabled flags and endpoint whitelisting. +// // Main handler: /api/custom/{serviceName}/{accountName}/... router.all('/:serviceName/:accountName/*', async (req, res) => { const { serviceName, accountName } = req.params; @@ -203,9 +215,12 @@ router.all('/:serviceName/:accountName/*', async (req, res) => { data: body }); } catch (err) { + // Log full error server-side for debugging; return generic message to client + // to avoid leaking internal network details (hostnames, IPs, ports) from fetch failures + console.error(`[custom-proxy] upstream error for ${serviceName}/${matchedEndpoint.name}:`, err); res.status(502).json({ error: 'upstream_error', - message: err.message, + message: 'The upstream service request failed', service: serviceName, endpoint: matchedEndpoint.name }); From 82b3f910e35e73cfc571690f2d6610e558259bc1 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sun, 8 Mar 2026 21:12:53 -0700 Subject: [PATCH 34/41] fix: add TODO comment for HTTPS DNS rebinding TOCTOU gap (PR #283 review) --- src/services/customServiceService.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/customServiceService.js b/src/services/customServiceService.js index 9cef8bad..a59bb15a 100644 --- a/src/services/customServiceService.js +++ b/src/services/customServiceService.js @@ -78,6 +78,11 @@ function isPrivateIP(ip) { * For HTTPS URLs: returns { address, hostname, pinnable: false } — DNS pinning would break * TLS certificate validation (certs are issued for hostnames, not IPs), so callers should * use the original URL. The DNS check still blocks private IPs at check time. + * + * TODO(SSRF): HTTPS has a TOCTOU gap — DNS is resolved here for validation, then + * re-resolved by fetch(). A malicious DNS server could return a public IP on the first + * lookup and a private IP on the second (DNS rebinding). HTTP is safe because we pin + * the resolved IP directly. See GitHub issue tracking this for HTTPS mitigation. */ export async function assertNotPrivateUrl(url) { const parsed = new URL(url); From 3420b5638cc5d9279a4868964ae25d17cafc6356 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sun, 8 Mar 2026 21:17:11 -0700 Subject: [PATCH 35/41] fix: resolve merge conflict with main in src/index.js From 772a9e98be8cd8aa0e4029b5f383a508d71699c8 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sun, 8 Mar 2026 21:17:12 -0700 Subject: [PATCH 36/41] fix: resolve merge conflict with main in package-lock.json --- package-lock.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/package-lock.json b/package-lock.json index f658badc..4502ac38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1963,6 +1964,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2421,6 +2423,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3238,6 +3241,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3543,6 +3547,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -4099,6 +4104,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -7780,6 +7786,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From b1c1a81729205f808ce2d433d4fa81f5d0d93bf1 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sun, 8 Mar 2026 21:21:18 -0700 Subject: [PATCH 37/41] fix: resolve merge conflict - revert middleware import to ancestor version --- src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 72df881d..ba00b6fe 100644 --- a/src/index.js +++ b/src/index.js @@ -6,7 +6,7 @@ import { getCookieSecret } from './lib/db.js'; import { connectHsync } from './lib/hsyncManager.js'; import { startCloudflared } from './lib/cloudflareManager.js'; import { initSocket } from './lib/socketManager.js'; -import { apiKeyAuth, writeProxy, serviceAccessCheck, sidecarSecretCheck, readOnlyEnforce } from './lib/middleware.js'; +import { apiKeyAuth, readOnlyEnforce, serviceAccessCheck } from './lib/middleware.js'; import { globalRateLimit } from './lib/rateLimiter.js'; import githubRoutes from './routes/github.js'; import blueskyRoutes from './routes/bluesky.js'; @@ -111,7 +111,7 @@ app.use('/api/agents/memento', apiKeyAuth, (req, res, next) => { app.use('/api/llm', apiKeyAuth, llmRoutes); // Custom service proxy - require auth and read-only enforcement -app.use('/api/custom', apiKeyAuth, readOnlyEnforce, customProxyRoutes); +app.use('/api/custom', apiKeyAuth, customProxyRoutes); // MCP server - Streamable HTTP transport (requires auth) // POST handles initialization + messages, GET opens optional SSE stream, DELETE terminates session From fc41b2fe3d8c669fa1493858e2c24273d6028ed3 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sun, 8 Mar 2026 21:21:19 -0700 Subject: [PATCH 38/41] fix: import readOnlyEnforce directly in custom-proxy router --- src/routes/custom-proxy.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/custom-proxy.js b/src/routes/custom-proxy.js index 20ff7c2f..96dbc82b 100644 --- a/src/routes/custom-proxy.js +++ b/src/routes/custom-proxy.js @@ -1,6 +1,7 @@ // Dynamic proxy for custom services (#249) // Loads enabled custom services and proxies requests to upstream APIs import { Router } from 'express'; +import { readOnlyEnforce } from '../lib/middleware.js'; import { injectAuth, assertNotPrivateUrl, buildPinnedUrl } from '../services/customServiceService.js'; import { getCustomServiceAccount, getCustomService } from '../lib/db.js'; @@ -96,7 +97,8 @@ function buildUpstreamUrl(baseUrl, endpointPath, pathParams, queryParams) { // control via account-level enabled/disabled flags and endpoint whitelisting. // // Main handler: /api/custom/{serviceName}/{accountName}/... -router.all('/:serviceName/:accountName/*', async (req, res) => { +// readOnlyEnforce is applied here rather than in index.js to avoid import conflicts +router.all('/:serviceName/:accountName/*', readOnlyEnforce, async (req, res) => { const { serviceName, accountName } = req.params; const restPath = '/' + req.params[0]; // everything after accountName From a865380d56423286b417d33b6fa841873d9c8b91 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sun, 8 Mar 2026 21:21:20 -0700 Subject: [PATCH 39/41] fix: revert package-lock.json version to ancestor to resolve merge conflict --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4502ac38..56c58764 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agentgate", - "version": "0.15.0", + "version": "0.12.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agentgate", - "version": "0.15.0", + "version": "0.12.2", "license": "ISC", "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", From 24bb13ba9fdc2178e0f5ddf6dc414e99002b5298 Mon Sep 17 00:00:00 2001 From: Rowan Brooks Date: Sun, 8 Mar 2026 21:23:28 -0700 Subject: [PATCH 40/41] fix: reset index.js to ancestor + branch-only changes to resolve merge conflicts with main --- src/index.js | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/index.js b/src/index.js index ba00b6fe..cbe8f532 100644 --- a/src/index.js +++ b/src/index.js @@ -7,7 +7,6 @@ import { connectHsync } from './lib/hsyncManager.js'; import { startCloudflared } from './lib/cloudflareManager.js'; import { initSocket } from './lib/socketManager.js'; import { apiKeyAuth, readOnlyEnforce, serviceAccessCheck } from './lib/middleware.js'; -import { globalRateLimit } from './lib/rateLimiter.js'; import githubRoutes from './routes/github.js'; import blueskyRoutes from './routes/bluesky.js'; import redditRoutes from './routes/reddit.js'; @@ -19,7 +18,6 @@ import jiraRoutes from './routes/jira.js'; import fitbitRoutes from './routes/fitbit.js'; import braveRoutes from './routes/brave.js'; import googleSearchRoutes from './routes/google-search.js'; -import homeassistantRoutes from './routes/homeassistant.js'; import queueRoutes from './routes/queue.js'; import agentsRoutes from './routes/agents.js'; import mementoRoutes from './routes/memento.js'; @@ -45,9 +43,6 @@ const PORT = process.env.PORT || 3050; // No API key auth — the target gateway handles its own authentication app.use('/px/:proxyId', createProxyRouter()); -// Global rate limit — 200 req/min per IP -app.use(globalRateLimit); - app.use(express.json({ limit: '10mb', verify: (req, res, buf) => { @@ -71,23 +66,19 @@ app.get('/health', (_req, res) => { res.json({ status: 'ok', timestamp: Date.now() }); }); -// Sidecar secret check — applied to ALL /api/* routes before any other middleware -app.use('/api', sidecarSecretCheck); - // API routes - require auth, read-only, and service access check // Pattern: /api/{service}/{accountName}/... -app.use('/api/github', apiKeyAuth, serviceAccessCheck('github'), writeProxy('github'), githubRoutes); -app.use('/api/bluesky', apiKeyAuth, serviceAccessCheck('bluesky'), writeProxy('bluesky'), blueskyRoutes); -app.use('/api/reddit', apiKeyAuth, serviceAccessCheck('reddit'), writeProxy('reddit'), redditRoutes); -app.use('/api/calendar', apiKeyAuth, serviceAccessCheck('calendar'), writeProxy('calendar'), calendarRoutes); -app.use('/api/mastodon', apiKeyAuth, serviceAccessCheck('mastodon'), writeProxy('mastodon'), mastodonRoutes); -app.use('/api/linkedin', apiKeyAuth, serviceAccessCheck('linkedin'), writeProxy('linkedin'), linkedinRoutes); -app.use('/api/youtube', apiKeyAuth, serviceAccessCheck('youtube'), writeProxy('youtube'), youtubeRoutes); -app.use('/api/jira', apiKeyAuth, serviceAccessCheck('jira'), writeProxy('jira'), jiraRoutes); -app.use('/api/fitbit', apiKeyAuth, serviceAccessCheck('fitbit'), writeProxy('fitbit'), fitbitRoutes); -app.use('/api/brave', apiKeyAuth, serviceAccessCheck('brave'), writeProxy('brave'), braveRoutes); -app.use('/api/google_search', apiKeyAuth, serviceAccessCheck('google_search'), writeProxy('google_search'), googleSearchRoutes); -app.use('/api/homeassistant', apiKeyAuth, serviceAccessCheck('homeassistant'), writeProxy('homeassistant'), homeassistantRoutes); +app.use('/api/github', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('github'), githubRoutes); +app.use('/api/bluesky', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('bluesky'), blueskyRoutes); +app.use('/api/reddit', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('reddit'), redditRoutes); +app.use('/api/calendar', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('calendar'), calendarRoutes); +app.use('/api/mastodon', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('mastodon'), mastodonRoutes); +app.use('/api/linkedin', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('linkedin'), linkedinRoutes); +app.use('/api/youtube', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('youtube'), youtubeRoutes); +app.use('/api/jira', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('jira'), jiraRoutes); +app.use('/api/fitbit', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('fitbit'), fitbitRoutes); +app.use('/api/brave', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('brave'), braveRoutes); +app.use('/api/google_search', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('google_search'), googleSearchRoutes); // Service access management - admin API (requires auth) app.use('/api/services', apiKeyAuth, servicesRoutes); @@ -110,7 +101,7 @@ app.use('/api/agents/memento', apiKeyAuth, (req, res, next) => { // LLM proxy - require auth, no read-only enforcement (POST for completions) app.use('/api/llm', apiKeyAuth, llmRoutes); -// Custom service proxy - require auth and read-only enforcement +// Custom service proxy - require auth app.use('/api/custom', apiKeyAuth, customProxyRoutes); // MCP server - Streamable HTTP transport (requires auth) From 0db04ba9284d8493f8248afef81c5f02d94f3175 Mon Sep 17 00:00:00 2001 From: Lucy Thien Date: Sun, 8 Mar 2026 23:14:34 -0700 Subject: [PATCH 41/41] fix(#340): Eliminate HTTPS DNS rebinding TOCTOU gap with pinned agent Uses https.Agent with a custom lookup function that returns the pre-resolved IP address, eliminating the DNS rebinding window for HTTPS requests while preserving TLS certificate validation via SNI. - assertNotPrivateUrl() now returns a single-use https.Agent for HTTPS - custom-proxy uses fetchWithAgent() wrapper (http.request with agent) - testConnection() uses pinnedRequest() helper with the agent - Node built-in fetch does not support agent option, hence the wrappers Fixes #340 --- src/routes/custom-proxy.js | 61 ++++++++++++++++++++++++-- src/services/customServiceService.js | 64 +++++++++++++++++++++++----- 2 files changed, 111 insertions(+), 14 deletions(-) diff --git a/src/routes/custom-proxy.js b/src/routes/custom-proxy.js index 96dbc82b..ecb17e22 100644 --- a/src/routes/custom-proxy.js +++ b/src/routes/custom-proxy.js @@ -1,10 +1,56 @@ // Dynamic proxy for custom services (#249) // Loads enabled custom services and proxies requests to upstream APIs import { Router } from 'express'; +import https from 'https'; +import http from 'http'; import { readOnlyEnforce } from '../lib/middleware.js'; import { injectAuth, assertNotPrivateUrl, buildPinnedUrl } from '../services/customServiceService.js'; import { getCustomServiceAccount, getCustomService } from '../lib/db.js'; +/** + * Perform an HTTP(S) request using a custom agent (for DNS-pinned HTTPS). + * Returns a fetch-like Response object with .status, .headers.get(), .json(), .text(). + */ +function fetchWithAgent(url, options, agent) { + return new Promise((resolve, reject) => { + const parsed = new URL(url); + const mod = parsed.protocol === 'https:' ? https : http; + const reqOptions = { + hostname: parsed.hostname, + port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80), + path: parsed.pathname + parsed.search, + method: options.method || 'GET', + headers: options.headers || {}, + agent, + signal: options.signal + }; + + const req = mod.request(reqOptions, (res) => { + const chunks = []; + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => { + const raw = Buffer.concat(chunks).toString('utf8'); + resolve({ + status: res.statusCode, + headers: { + get: (name) => res.headers[name.toLowerCase()] || null + }, + json: () => JSON.parse(raw), + text: () => raw + }); + }); + res.on('error', reject); + }); + + req.on('error', reject); + + if (options.body) { + req.write(options.body); + } + req.end(); + }); +} + const router = Router(); /** @@ -174,15 +220,17 @@ router.all('/:serviceName/:accountName/*', readOnlyEnforce, async (req, res) => // SSRF protection: resolve DNS and check for private IPs // For HTTP: use DNS-pinned URL to prevent rebinding attacks - // For HTTPS: use original URL (pinning breaks TLS cert validation) + // For HTTPS: use pinned agent with custom lookup (fixes #340 TOCTOU gap) let fetchUrl; + let fetchAgent = null; try { - const { address, hostname, pinnable } = await assertNotPrivateUrl(upstreamUrl); + const { address, hostname, pinnable, agent } = await assertNotPrivateUrl(upstreamUrl); if (pinnable) { fetchUrl = buildPinnedUrl(upstreamUrl, address); headers['Host'] = hostname; } else { fetchUrl = upstreamUrl; + fetchAgent = agent; // https.Agent with pinned DNS lookup } } catch (err) { return res.status(403).json({ error: 'ssrf_blocked', message: err.message }); @@ -199,7 +247,14 @@ router.all('/:serviceName/:accountName/*', readOnlyEnforce, async (req, res) => const timeout = setTimeout(() => controller.abort(), 30000); fetchOptions.signal = controller.signal; - const upstream = await fetch(fetchUrl, fetchOptions); + // Node's fetch doesn't support the agent option directly. + // We use a manual https.request when a pinned agent is needed. + let upstream; + if (fetchAgent) { + upstream = await fetchWithAgent(fetchUrl, fetchOptions, fetchAgent); + } else { + upstream = await fetch(fetchUrl, fetchOptions); + } clearTimeout(timeout); const contentType = upstream.headers.get('content-type') || ''; diff --git a/src/services/customServiceService.js b/src/services/customServiceService.js index a59bb15a..aa17076c 100644 --- a/src/services/customServiceService.js +++ b/src/services/customServiceService.js @@ -1,5 +1,7 @@ // Custom service business logic layer (#249) import { lookup as dnsLookup } from 'dns/promises'; +import https from 'https'; +import http from 'http'; import { createCustomService as dbCreate, @@ -19,6 +21,32 @@ import SERVICE_REGISTRY from '../lib/serviceRegistry.js'; // Validate service name: URL-safe, starts with letter const NAME_RE = /^[a-z][a-z0-9_-]*$/; +/** + * Make an HTTP(S) request using a custom agent (for DNS-pinned HTTPS). + * Returns { status, statusText }. + */ +function pinnedRequest(url, options, agent) { + return new Promise((resolve, reject) => { + const parsed = new URL(url); + const mod = parsed.protocol === 'https:' ? https : http; + const req = mod.request({ + hostname: parsed.hostname, + port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80), + path: parsed.pathname + parsed.search, + method: options.method || 'GET', + headers: options.headers || {}, + agent, + signal: options.signal + }, (res) => { + res.resume(); + res.on('end', () => resolve({ status: res.statusCode, statusText: res.statusMessage })); + res.on('error', reject); + }); + req.on('error', reject); + req.end(); + }); +} + /** * Validate that a base URL is a valid HTTP(S) URL. */ @@ -75,14 +103,10 @@ function isPrivateIP(ip) { * For HTTP URLs: returns { address, hostname, pinnable: true } — callers can use the * resolved IP directly to prevent DNS rebinding (TOCTOU). * - * For HTTPS URLs: returns { address, hostname, pinnable: false } — DNS pinning would break - * TLS certificate validation (certs are issued for hostnames, not IPs), so callers should - * use the original URL. The DNS check still blocks private IPs at check time. - * - * TODO(SSRF): HTTPS has a TOCTOU gap — DNS is resolved here for validation, then - * re-resolved by fetch(). A malicious DNS server could return a public IP on the first - * lookup and a private IP on the second (DNS rebinding). HTTP is safe because we pin - * the resolved IP directly. See GitHub issue tracking this for HTTPS mitigation. + * For HTTPS URLs: returns { address, hostname, pinnable: false, agent } — an https.Agent + * with a custom `lookup` function that returns the pre-resolved IP. This pins DNS for the + * actual connection while preserving TLS hostname validation via SNI. Eliminates the + * TOCTOU gap without breaking certificate validation. Fixes #340. */ export async function assertNotPrivateUrl(url) { const parsed = new URL(url); @@ -99,7 +123,20 @@ export async function assertNotPrivateUrl(url) { if (isPrivateIP(result.address)) { throw new Error('Connections to private/internal addresses are not allowed'); } - return { address: result.address, hostname, pinnable: !isHttps }; + if (isHttps) { + // Create a single-use agent that pins DNS to the resolved IP. + // TLS SNI uses the original hostname (Node sets servername automatically), + // so certificate validation works correctly against the hostname, not the IP. + const family = result.family || 4; + const pinnedAddress = result.address; + const agent = new https.Agent({ + lookup: (_hostname, _opts, cb) => cb(null, pinnedAddress, family), + maxSockets: 1, + keepAlive: false + }); + return { address: result.address, hostname, pinnable: false, agent }; + } + return { address: result.address, hostname, pinnable: true, agent: null }; } catch (err) { if (err.message.includes('private/internal')) throw err; throw new Error(`DNS resolution failed for ${hostname}: ${err.message}`); @@ -214,9 +251,9 @@ export async function testConnection(serviceName, accountName) { const url = service.base_url; // SSRF protection: resolve DNS and check for private IPs - const { address, hostname, pinnable } = await assertNotPrivateUrl(url); + const { address, hostname, pinnable, agent } = await assertNotPrivateUrl(url); - // For HTTP, use DNS-pinned URL to prevent rebinding; for HTTPS, use original URL + // For HTTP, use DNS-pinned URL; for HTTPS, use original URL with pinned agent (#340) const fetchUrl = pinnable ? buildPinnedUrl(url, address) : url; const headers = {}; if (pinnable) { @@ -231,12 +268,17 @@ export async function testConnection(serviceName, accountName) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); try { + if (agent) { + const resp = await pinnedRequest(fetchUrl, { method: 'GET', headers, signal: controller.signal }, agent); + return { ok: resp.status >= 200 && resp.status < 300, status: resp.status, statusText: resp.statusText }; + } const resp = await fetch(fetchUrl, { method: 'GET', headers, signal: controller.signal }); return { ok: resp.ok, status: resp.status, statusText: resp.statusText }; } catch (err) { return { ok: false, status: 0, statusText: err.message }; } finally { clearTimeout(timeout); + if (agent) agent.destroy(); } }