import { proxyToInstance, safeProxyPath, ProxyRequestError } from './service.js' import { defaultProxyPolicy } from './policy.js' import { ConfirmationStore, confirmationBinding } from './confirmations.js' function requestTarget(request, rest) { const path = safeProxyPath(rest) const query = request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : '' return `${path}${query}` } export function registerProxyRoutes(app, { findClient, proxy = proxyToInstance, policy = defaultProxyPolicy, configStore, confirmationStore = new ConfirmationStore(), }) { app.post('/api/proxy-confirmations/prepare', async (request, reply) => { try { const input = request.body || {} const client = findClient(input.instanceId, reply) if (!client) return const method = String(input.method || '').toUpperCase() const target = String(input.path || '') const parsed = new URL(target, 'http://confirmation.local') if (parsed.origin !== 'http://confirmation.local') throw new Error('invalid confirmation path') const decision = policy.authorize(method, parsed.pathname) if (!decision.allowed) return reply.code(decision.statusCode).send({ error: decision.reason }) if (!decision.dangerous) return reply.code(400).send({ error: 'confirmation is only available for dangerous writes' }) const binding = confirmationBinding({ instance: client.instance, revision: configStore.snapshot.revision, method, target, body: input.body }) return confirmationStore.prepare(binding) } catch (error) { return reply.code(400).send({ error: error.message }) } }) app.all('/api/proxy/:id/*', async (request, reply) => { const client = findClient(request.params.id, reply) if (!client) return try { const target = requestTarget(request, request.params['*']) const pathname = new URL(target, 'http://proxy.local').pathname const decision = policy.authorize(request.method, pathname) if (!decision.allowed) return reply.code(decision.statusCode).send({ error: decision.reason }) if (decision.dangerous) { const binding = confirmationBinding({ instance: client.instance, revision: configStore.snapshot.revision, method: request.method, target, body: request.body }) const token = request.headers['x-confirmation-token'] if (!token) return reply.code(428).send({ error: 'confirmation token required' }) if (!confirmationStore.consume(token, binding)) return reply.code(403).send({ error: 'invalid, expired, replayed, or mismatched confirmation token' }) } return await proxy({ client, request, reply, rest: request.params['*'] }) } catch (error) { if (error instanceof ProxyRequestError && error.code === 'invalid_path') return reply.code(400).send({ error: 'invalid proxy path' }) if (error instanceof ProxyRequestError && error.code === 'unsupported_content_type') return reply.code(415).send({ error: 'unsupported proxy content type' }) request.log?.error?.({ err: error }, 'upstream proxy failed') return reply.code(502).send({ error: 'upstream request failed' }) } }) }