// P4 client contract; endpoint/payload cases are probe-pending on PIN8.0.42. // Catches management key/body/query values leaking into receipts, redirects/retries. import test from 'node:test'; import assert from 'node:assert/strict'; import http from 'node:http'; import { ManagementClient } from '../src/client.mjs'; async function setup(t, handler) { const calls = [], receipts = []; const server = http.createServer(async (req, res) => { let body = ''; for await (const c of req) body += c; calls.push({ method: req.method, path: req.url, auth: req.headers.authorization, body }); const answer = handler?.(req, body) ?? { status: 200, body: { status: 'ok' } }; res.writeHead(answer.status, { 'content-type': 'application/json', ...answer.headers }); res.end(JSON.stringify(answer.body)); }); await new Promise(r => server.listen(0, '127.0.0.1', r)); t.after(() => new Promise(r => server.close(r))); const client = new ManagementClient({ baseUrl: `http://127.0.0.1:${server.address().port}`, key: 'p4-management', receipt: r => receipts.push(r) }); return { client, calls, receipts }; } test('OAuth management calls keep URL and state out of receipts', async t => { const s = await setup(t); assert.equal(typeof s.client.oauthStart, 'function'); await s.client.oauthStart('claude'); await s.client.oauthCallback('http://localhost/callback?state=p4-state&code=p4-code'); await s.client.oauthStatus('p4-state'); await s.client.oauthCancel('p4-state'); assert.deepEqual(s.calls.map(({ method, path }) => [method, path]), [ ['GET', '/v8/management/oauth/auth-url?provider=claude'], ['POST', '/v8/management/oauth/callback'], ['GET', '/v8/management/oauth/status?state=p4-state'], ['DELETE', '/v8/management/oauth/session?state=p4-state'], ]); assert.ok(s.calls.every(c => c.auth === 'Bearer p4-management')); for (const value of ['p4-state', 'p4-code', 'localhost', 'p4-management']) assert.equal(JSON.stringify(s.receipts).includes(value), false); }); test('upstream key goes only in request body and upstream refusal body is discarded', async t => { const s = await setup(t, () => ({ status: 400, body: { error: 'p4-canary-key' } })); assert.equal(typeof s.client.putUpstreamGroups, 'function'); await assert.rejects(s.client.putUpstreamGroups([{ name: 'z.ai', keys: [{ 'api-key': 'p4-canary-key' }] }]), e => !e.message.includes('p4-canary-key')); assert.equal(s.calls.length, 1); assert.ok(s.calls[0].body.includes('p4-canary-key')); assert.equal(JSON.stringify(s.receipts).includes('p4-canary-key'), false); }); test('redirected management calls are not followed or retried', async t => { const s = await setup(t, () => ({ status: 302, headers: { location: 'http://127.0.0.1:8317/' }, body: {} })); assert.equal(typeof s.client.getCredentials, 'function'); await assert.rejects(s.client.getCredentials(), { code: 'proxy-client-redirect' }); assert.equal(s.calls.length, 1); }); test('credential model read encodes the name and keeps it out of receipts', async t => { const s = await setup(t, () => ({ status: 200, body: { models: [{ id: 'fixture-model' }] } })); assert.deepEqual(await s.client.getCredentialModels('fixture name.json'), { models: [{ id: 'fixture-model' }] }); assert.equal(s.calls[0].path, '/v8/management/credentials/models?name=fixture%20name.json'); assert.equal(JSON.stringify(s.receipts).includes('fixture name.json'), false); for (const name of ['', 'x\n', 'x'.repeat(513)]) await assert.rejects(async () => s.client.getCredentialModels(name), { code: 'proxy-client-invalid' }); });