diff --git a/scripts/langgraph-proxy.spec.ts b/scripts/langgraph-proxy.spec.ts index 8ba48594e..09b45307b 100644 --- a/scripts/langgraph-proxy.spec.ts +++ b/scripts/langgraph-proxy.spec.ts @@ -150,6 +150,30 @@ describe('createProxyHandler', () => { expect(res._status).toBe(200); }); + // Regression: `/_proxy_debug` was an unauthenticated early-return that + // disclosed a prefix of LANGSMITH_API_KEY, the upstream deployment URL, and + // environment facts to any caller. It sat above the origin/rate-limit gates, + // and the origin allowlist only rejects when an Origin header is present, so + // a plain curl reached it on production. It must stay a normal proxied path. + it('does not answer /_proxy_debug locally or disclose key material', async () => { + const fetchMock = vi.spyOn(global, 'fetch').mockResolvedValue( + new Response('{"detail":"Not Found"}', { status: 404, headers: { 'content-type': 'application/json' } }), + ); + const handler = createProxyHandler({ backendUrl: DEFAULT_BACKEND }); + const res = makeRes(); + await handler({ method: 'GET', headers: { host: 'demo.threadplane.ai' }, url: '/api/_proxy_debug', query: {} } as never, res as never); + + // Forwarded upstream like any other path rather than short-circuited here. + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock.mock.calls[0]![0]).toBe(`${DEFAULT_BACKEND}/_proxy_debug`); + + // Nothing the proxy itself wrote may carry the key or its prefix. + const written = JSON.stringify([res.json.mock.calls, res.send.mock.calls, res.write.mock.calls]); + expect(written).not.toContain('test-key-123'); + expect(written).not.toContain('test-key-1'); + expect(written).not.toContain('apiKeyPrefix'); + }); + it('strips the catch-all query param but keeps real query params', async () => { const fetchMock = vi.spyOn(global, 'fetch').mockResolvedValue( new Response('{}', { status: 200, headers: { 'content-type': 'application/json' } }), diff --git a/scripts/langgraph-proxy.ts b/scripts/langgraph-proxy.ts index 4b387079c..9728be537 100644 --- a/scripts/langgraph-proxy.ts +++ b/scripts/langgraph-proxy.ts @@ -152,29 +152,6 @@ export function createProxyHandler(config: ProxyConfig = {}): (req: VercelReques const backendUrl = resolveBackend(req.headers.referer); const targetUrl = `${backendUrl}${apiPath}${cleanSearch}`; - // Debug endpoint — confirms the proxy is wired without hitting the upstream. - if (apiPath === '/_proxy_debug') { - res.status(200).json({ - method: req.method, - url: req.url, - apiPath, - targetUrl, - backendUrl, - referer: req.headers.referer, - query: req.query, - hasApiKey: !!apiKey, - apiKeyPrefix: apiKey.substring(0, 10), - hasDatabaseUrl: !!process.env['DATABASE_URL'], - rateLimitConfigured: !!config.checkRateLimit, - instanceId: (() => { - const g = globalThis as { __instanceId?: string }; - if (!g.__instanceId) g.__instanceId = Math.random().toString(36).slice(2, 10); - return g.__instanceId; - })(), - }); - return; - } - // Body-size cap. Fast-fail before rate-limit + upstream fetch. if (config.maxBodyBytes !== undefined) { const cl = req.headers['content-length'];