From b394ad4bcec233b5490594ac5ffc64e4edf31c61 Mon Sep 17 00:00:00 2001 From: Tate Lyman Date: Fri, 22 May 2026 17:14:53 -0500 Subject: [PATCH] Harden x402 paid-action example responses --- examples/x402-paid-action-receipt/server.js | 5 ++++- package-lock.json | 10 +++++++++ tests/x402-paid-action-receipt.test.js | 24 ++++++++++++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/examples/x402-paid-action-receipt/server.js b/examples/x402-paid-action-receipt/server.js index bfe5b67..3dc870e 100644 --- a/examples/x402-paid-action-receipt/server.js +++ b/examples/x402-paid-action-receipt/server.js @@ -10,7 +10,10 @@ function dedupeKey(requestId, paymentId) { } function sendJson(res, statusCode, body) { - res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + res.writeHead(statusCode, { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store' + }); res.end(JSON.stringify(body, null, 2)); } diff --git a/package-lock.json b/package-lock.json index 2ae9904..5d77aeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "commandlayer-org", "version": "1.1.0", "dependencies": { + "@neondatabase/serverless": "^1.0.0", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "ethers": "^6.16.0", @@ -20,6 +21,15 @@ "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", "license": "MIT" }, + "node_modules/@neondatabase/serverless": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-1.1.0.tgz", + "integrity": "sha512-r3ZZhRjEcfEdKIZnoB1RusNgvHuaBRqfCzV4Gi+5A9yUX0S4HTws/ASWqt13wL4y4I+0rqsWGdA2w7EQXHi3+Q==", + "license": "MIT", + "engines": { + "node": ">=19.0.0" + } + }, "node_modules/@noble/curves": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", diff --git a/tests/x402-paid-action-receipt.test.js b/tests/x402-paid-action-receipt.test.js index 1212a92..1941bcc 100644 --- a/tests/x402-paid-action-receipt.test.js +++ b/tests/x402-paid-action-receipt.test.js @@ -20,7 +20,7 @@ function postJson(port, path, body) { raw += chunk; }); res.on('end', () => { - resolve({ statusCode: res.statusCode, body: JSON.parse(raw) }); + resolve({ statusCode: res.statusCode, headers: res.headers, body: JSON.parse(raw) }); }); } ); @@ -55,6 +55,7 @@ test('paid-action emits receipt and enforces idempotency', async () => { const first = await postJson(port, '/paid-action', payload); assert.equal(first.statusCode, 200); + assert.equal(first.headers['cache-control'], 'no-store'); assert.equal(first.body.duplicate, false); assert.equal(first.body.receipt.request_id, 'req_test_1'); assert.equal(first.body.receipt.payment_id, 'pay_test_1'); @@ -63,8 +64,29 @@ test('paid-action emits receipt and enforces idempotency', async () => { const second = await postJson(port, '/paid-action', payload); assert.equal(second.statusCode, 200); + assert.equal(second.headers['cache-control'], 'no-store'); assert.equal(second.body.duplicate, true); assert.equal(second.body.receipt.receipt_id, first.body.receipt.receipt_id); await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve()))); }); + +test('paid-action payment errors are not cacheable', async () => { + const server = createServer(); + await new Promise((resolve) => server.listen(0, resolve)); + const port = server.address().port; + + const res = await postJson(port, '/paid-action', { + paid_action_request: { + request_id: 'req_missing_payment', + action: 'summarize.text', + input: { text: 'Payment is intentionally missing.' }, + payment: { required: true, plan: 'pro', max_amount: '0.05', currency: 'USD' } + } + }); + + assert.equal(res.statusCode, 402); + assert.equal(res.headers['cache-control'], 'no-store'); + + await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve()))); +});