@dupecom/botcha-cloudflare 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +74 -9
  2. package/dist/agent-auth.d.ts +129 -0
  3. package/dist/agent-auth.d.ts.map +1 -0
  4. package/dist/agent-auth.js +210 -0
  5. package/dist/agents.d.ts +10 -0
  6. package/dist/agents.d.ts.map +1 -1
  7. package/dist/agents.js +51 -1
  8. package/dist/app-gate.d.ts +6 -0
  9. package/dist/app-gate.d.ts.map +1 -0
  10. package/dist/app-gate.js +69 -0
  11. package/dist/apps.d.ts +9 -0
  12. package/dist/apps.d.ts.map +1 -1
  13. package/dist/apps.js +26 -0
  14. package/dist/dashboard/account.d.ts +63 -0
  15. package/dist/dashboard/account.d.ts.map +1 -0
  16. package/dist/dashboard/account.js +488 -0
  17. package/dist/dashboard/api.js +15 -68
  18. package/dist/dashboard/auth.d.ts.map +1 -1
  19. package/dist/dashboard/auth.js +14 -14
  20. package/dist/dashboard/docs.d.ts.map +1 -1
  21. package/dist/dashboard/docs.js +146 -3
  22. package/dist/dashboard/layout.d.ts.map +1 -1
  23. package/dist/dashboard/layout.js +2 -2
  24. package/dist/dashboard/mcp-setup.d.ts +15 -0
  25. package/dist/dashboard/mcp-setup.d.ts.map +1 -0
  26. package/dist/dashboard/mcp-setup.js +391 -0
  27. package/dist/dashboard/showcase.d.ts +6 -10
  28. package/dist/dashboard/showcase.d.ts.map +1 -1
  29. package/dist/dashboard/showcase.js +67 -991
  30. package/dist/dashboard/whitepaper.d.ts.map +1 -1
  31. package/dist/dashboard/whitepaper.js +42 -4
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +452 -52
  35. package/dist/mcp.d.ts +20 -0
  36. package/dist/mcp.d.ts.map +1 -0
  37. package/dist/mcp.js +1290 -0
  38. package/dist/oauth-agent.d.ts +130 -0
  39. package/dist/oauth-agent.d.ts.map +1 -0
  40. package/dist/oauth-agent.js +194 -0
  41. package/dist/static.d.ts +732 -1
  42. package/dist/static.d.ts.map +1 -1
  43. package/dist/static.js +646 -2
  44. package/dist/tap-a2a-routes.d.ts +355 -0
  45. package/dist/tap-a2a-routes.d.ts.map +1 -0
  46. package/dist/tap-a2a-routes.js +475 -0
  47. package/dist/tap-a2a.d.ts +199 -0
  48. package/dist/tap-a2a.d.ts.map +1 -0
  49. package/dist/tap-a2a.js +502 -0
  50. package/dist/tap-agents.d.ts +15 -0
  51. package/dist/tap-agents.d.ts.map +1 -1
  52. package/dist/tap-agents.js +31 -1
  53. package/dist/tap-ans-routes.d.ts +302 -0
  54. package/dist/tap-ans-routes.d.ts.map +1 -0
  55. package/dist/tap-ans-routes.js +535 -0
  56. package/dist/tap-ans.d.ts +241 -0
  57. package/dist/tap-ans.d.ts.map +1 -0
  58. package/dist/tap-ans.js +481 -0
  59. package/dist/tap-delegation-routes.d.ts.map +1 -1
  60. package/dist/tap-delegation-routes.js +11 -0
  61. package/dist/tap-did.d.ts +140 -0
  62. package/dist/tap-did.d.ts.map +1 -0
  63. package/dist/tap-did.js +262 -0
  64. package/dist/tap-oidca-routes.d.ts +383 -0
  65. package/dist/tap-oidca-routes.d.ts.map +1 -0
  66. package/dist/tap-oidca-routes.js +597 -0
  67. package/dist/tap-oidca.d.ts +288 -0
  68. package/dist/tap-oidca.d.ts.map +1 -0
  69. package/dist/tap-oidca.js +461 -0
  70. package/dist/tap-routes.d.ts +24 -8
  71. package/dist/tap-routes.d.ts.map +1 -1
  72. package/dist/tap-routes.js +169 -23
  73. package/dist/tap-vc-routes.d.ts +358 -0
  74. package/dist/tap-vc-routes.d.ts.map +1 -0
  75. package/dist/tap-vc-routes.js +367 -0
  76. package/dist/tap-vc.d.ts +125 -0
  77. package/dist/tap-vc.d.ts.map +1 -0
  78. package/dist/tap-vc.js +245 -0
  79. package/dist/tap-x402-routes.d.ts +89 -0
  80. package/dist/tap-x402-routes.d.ts.map +1 -0
  81. package/dist/tap-x402-routes.js +579 -0
  82. package/dist/tap-x402.d.ts +222 -0
  83. package/dist/tap-x402.d.ts.map +1 -0
  84. package/dist/tap-x402.js +546 -0
  85. package/dist/webhooks.d.ts +99 -0
  86. package/dist/webhooks.d.ts.map +1 -0
  87. package/dist/webhooks.js +642 -0
  88. package/package.json +3 -1
@@ -0,0 +1,579 @@
1
+ /**
2
+ * x402 Payment Gating — Route Handlers
3
+ *
4
+ * Exposes BOTCHA's x402-compliant API endpoints:
5
+ *
6
+ * POST /v1/x402/verify-payment — Facilitator: verify a payment proof
7
+ * GET /v1/x402/challenge — Pay-for-verification (402 → X-Payment → token)
8
+ * GET /agent-only/x402 — Demo: requires BOTCHA token + x402 payment
9
+ * POST /v1/x402/webhook — Settlement notifications from facilitators
10
+ *
11
+ * All endpoints are built for Cloudflare Workers (Hono framework, no Node APIs).
12
+ */
13
+ import { extractBearerToken, verifyToken, getSigningPublicKeyJWK } from './auth.js';
14
+ function getVerificationPublicKey(env) {
15
+ const rawSigningKey = env?.JWT_SIGNING_KEY;
16
+ if (!rawSigningKey)
17
+ return undefined;
18
+ try {
19
+ const signingKey = JSON.parse(rawSigningKey);
20
+ return getSigningPublicKeyJWK(signingKey);
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ import { buildPaymentRequiredDescriptor, parsePaymentHeader, verifyX402Payment, issueTokenForPayment, buildPaymentResponseHeader, storePaymentRecord, processWebhookEvent, VERIFICATION_PRICE_USDC_UNITS, VERIFICATION_PRICE_HUMAN, BASE_CHAIN_ID, BOTCHA_WALLET, USDC_BASE_ADDRESS, PAYMENT_DEADLINE_SECONDS, } from './tap-x402.js';
27
+ // ============ HELPERS ============
28
+ function getSigningKey(env) {
29
+ if (!env.JWT_SIGNING_KEY)
30
+ return undefined;
31
+ try {
32
+ return JSON.parse(env.JWT_SIGNING_KEY);
33
+ }
34
+ catch {
35
+ console.error('Failed to parse JWT_SIGNING_KEY — falling back to HS256');
36
+ return undefined;
37
+ }
38
+ }
39
+ function getPublicKey(env) {
40
+ const sk = getSigningKey(env);
41
+ return sk ? getSigningPublicKeyJWK(sk) : undefined;
42
+ }
43
+ /**
44
+ * Resolve BOTCHA wallet from env (allows override via BOTCHA_PAYMENT_WALLET).
45
+ */
46
+ function getWallet(env) {
47
+ return env.BOTCHA_PAYMENT_WALLET || BOTCHA_WALLET;
48
+ }
49
+ /**
50
+ * Resolve app_id from query string, header, or JWT claim.
51
+ * Returns undefined if not present.
52
+ */
53
+ function extractAppId(c) {
54
+ return (c.req.query('app_id') ||
55
+ c.req.header('x-app-id') ||
56
+ undefined);
57
+ }
58
+ // ============ ROUTE HANDLERS ============
59
+ /**
60
+ * POST /v1/x402/verify-payment
61
+ *
62
+ * Acts as a lightweight x402-compatible facilitator.
63
+ * Accepts a payment proof (base64-encoded X-Payment payload) and
64
+ * verifies: structure, recipient, amount, deadline, nonce, signature.
65
+ *
66
+ * Request body:
67
+ * { payment: "<base64-encoded X402PaymentProof>" }
68
+ * OR submit the raw X-Payment header value directly in the body field.
69
+ *
70
+ * Response:
71
+ * 200 { verified: true, txHash, payer, amount, network }
72
+ * 400 { verified: false, error, errorCode }
73
+ */
74
+ export async function verifyPaymentRoute(c) {
75
+ try {
76
+ // Auth check first — payment verification is a privileged operation
77
+ const authHeader = c.req.header('authorization');
78
+ const token = extractBearerToken(authHeader);
79
+ if (!token) {
80
+ return c.json({
81
+ verified: false,
82
+ error: 'UNAUTHORIZED',
83
+ message: 'Bearer token required. Get a token via the BOTCHA challenge flow.',
84
+ }, 401);
85
+ }
86
+ const publicKey = getVerificationPublicKey(c.env);
87
+ const tokenResult = await verifyToken(token, c.env.JWT_SECRET, c.env, undefined, publicKey);
88
+ if (!tokenResult.valid) {
89
+ return c.json({
90
+ verified: false,
91
+ error: 'INVALID_TOKEN',
92
+ message: 'Bearer token is invalid or expired',
93
+ }, 401);
94
+ }
95
+ const body = await c.req.json().catch(() => ({}));
96
+ const paymentHeader = body.payment || c.req.header('x-payment');
97
+ if (!paymentHeader) {
98
+ return c.json({
99
+ verified: false,
100
+ error: 'Missing payment proof. Provide base64-encoded X-Payment value in body.payment or X-Payment header.',
101
+ errorCode: 'MISSING_PAYMENT',
102
+ hint: 'Base64-encode a JSON object: { scheme: "exact", network: "eip155:8453", payload: { from, to, value, validAfter, validBefore, nonce, signature } }',
103
+ }, 400);
104
+ }
105
+ const proof = parsePaymentHeader(paymentHeader);
106
+ if (!proof) {
107
+ return c.json({
108
+ verified: false,
109
+ error: 'Invalid payment proof format. Must be base64-encoded JSON matching X402PaymentProof schema.',
110
+ errorCode: 'INVALID_FORMAT',
111
+ }, 400);
112
+ }
113
+ const result = await verifyX402Payment(proof, c.env.NONCES, {
114
+ requiredRecipient: body.required_recipient || getWallet(c.env),
115
+ requiredAmount: body.required_amount || VERIFICATION_PRICE_USDC_UNITS,
116
+ });
117
+ if (!result.verified) {
118
+ return c.json({
119
+ verified: false,
120
+ error: result.error,
121
+ errorCode: result.errorCode,
122
+ }, 400);
123
+ }
124
+ // Return x402-compliant success response
125
+ c.header('X-Payment-Response', buildPaymentResponseHeader(result));
126
+ return c.json({
127
+ verified: true,
128
+ txHash: result.txHash,
129
+ payer: result.payer,
130
+ amount: result.amount,
131
+ network: result.network,
132
+ timestamp: new Date().toISOString(),
133
+ });
134
+ }
135
+ catch (error) {
136
+ console.error('verify-payment error:', error);
137
+ return c.json({
138
+ verified: false,
139
+ error: 'Internal verification error',
140
+ errorCode: 'INTERNAL_ERROR',
141
+ }, 500);
142
+ }
143
+ }
144
+ /**
145
+ * GET /v1/x402/challenge
146
+ *
147
+ * The flagship pay-for-verification endpoint.
148
+ *
149
+ * Without X-Payment header → 402 Payment Required
150
+ * With valid X-Payment header → 200 with BOTCHA access_token
151
+ *
152
+ * This allows agents to get a BOTCHA verification token by paying
153
+ * $0.001 USDC instead of solving a challenge.
154
+ *
155
+ * x402 standard flow:
156
+ * 1. Agent: GET /v1/x402/challenge
157
+ * 2. Server: 402 + X-Payment-Required: { amount, payTo, asset, ... }
158
+ * 3. Agent: signs ERC-3009 transferWithAuthorization
159
+ * 4. Agent: GET /v1/x402/challenge + X-Payment: <base64-proof>
160
+ * 5. Server: 200 + { access_token, ... } + X-Payment-Response: { success, txHash }
161
+ */
162
+ export async function x402ChallengeRoute(c) {
163
+ try {
164
+ const resource = '/v1/x402/challenge';
165
+ const appId = extractAppId(c);
166
+ const wallet = getWallet(c.env);
167
+ // Check for X-Payment header (payment proof from agent)
168
+ const paymentHeader = c.req.header('x-payment');
169
+ if (!paymentHeader) {
170
+ // No payment → return 402 with payment requirements
171
+ const descriptor = buildPaymentRequiredDescriptor(resource, {
172
+ description: `Pay ${VERIFICATION_PRICE_HUMAN} USDC to receive a BOTCHA verified agent token. Skip the challenge, pay to prove you're a trusted agent.`,
173
+ payTo: wallet,
174
+ appId,
175
+ });
176
+ return c.json({
177
+ error: 'PAYMENT_REQUIRED',
178
+ message: `This endpoint requires a ${VERIFICATION_PRICE_HUMAN} USDC payment on Base to issue a BOTCHA verification token.`,
179
+ x402: descriptor,
180
+ instructions: [
181
+ '1. Encode payment proof as base64 JSON matching X402PaymentProof schema',
182
+ '2. Retry this GET request with header: X-Payment: <base64-encoded-proof>',
183
+ '3. Receive access_token valid for 1 hour',
184
+ ],
185
+ alternative: 'Solve a free challenge instead: GET /v1/challenges?app_id=...',
186
+ }, 402, {
187
+ 'X-Payment-Required': JSON.stringify(descriptor),
188
+ 'X-Payment-Scheme': 'exact',
189
+ 'Content-Type': 'application/json',
190
+ });
191
+ }
192
+ // Parse payment proof
193
+ const proof = parsePaymentHeader(paymentHeader);
194
+ if (!proof) {
195
+ return c.json({
196
+ verified: false,
197
+ error: 'Invalid X-Payment header. Must be base64-encoded X402PaymentProof JSON.',
198
+ errorCode: 'INVALID_PAYMENT_FORMAT',
199
+ }, 400);
200
+ }
201
+ // Verify payment
202
+ const verification = await verifyX402Payment(proof, c.env.NONCES, {
203
+ requiredRecipient: wallet,
204
+ requiredAmount: VERIFICATION_PRICE_USDC_UNITS,
205
+ });
206
+ if (!verification.verified) {
207
+ return c.json({
208
+ verified: false,
209
+ error: verification.error,
210
+ errorCode: verification.errorCode,
211
+ }, 402, {
212
+ 'X-Payment-Required': JSON.stringify(buildPaymentRequiredDescriptor(resource, { payTo: wallet, appId })),
213
+ });
214
+ }
215
+ // Payment verified — issue BOTCHA token
216
+ const signingKey = getSigningKey(c.env);
217
+ const tokenStart = Date.now();
218
+ const tokenResult = await issueTokenForPayment(c.env.CHALLENGES, c.env, {
219
+ payer: verification.payer,
220
+ paymentId: verification.txHash,
221
+ appId,
222
+ audience: 'botcha-x402-verified',
223
+ solveTimeMs: 0, // payment-based, no solve time
224
+ }, signingKey);
225
+ const issuedMs = Date.now() - tokenStart;
226
+ // Store payment record
227
+ const paymentRecord = {
228
+ payment_id: verification.txHash,
229
+ payer: verification.payer,
230
+ amount: verification.amount,
231
+ network: verification.network,
232
+ tx_hash: verification.txHash,
233
+ resource,
234
+ nonce: proof.payload.nonce,
235
+ botcha_app_id: appId,
236
+ access_token: tokenResult.access_token,
237
+ verified_at: Math.floor(Date.now() / 1000),
238
+ status: 'verified',
239
+ };
240
+ await storePaymentRecord(c.env.NONCES, paymentRecord);
241
+ // Set x402 response headers
242
+ c.header('X-Payment-Response', buildPaymentResponseHeader(verification));
243
+ return c.json({
244
+ success: true,
245
+ verified: true,
246
+ method: 'x402-payment',
247
+ message: `Payment verified (${verification.amount} USDC units on ${verification.network}). BOTCHA token issued.`,
248
+ // === Token ===
249
+ access_token: tokenResult.access_token,
250
+ refresh_token: tokenResult.refresh_token,
251
+ expires_in: tokenResult.expires_in,
252
+ refresh_expires_in: tokenResult.refresh_expires_in,
253
+ token_type: 'Bearer',
254
+ // === Payment info ===
255
+ payment: {
256
+ payer: verification.payer,
257
+ txHash: verification.txHash,
258
+ amount: verification.amount,
259
+ amountHuman: `${(parseInt(verification.amount || '0') / 1e6).toFixed(6)} USDC`,
260
+ network: verification.network,
261
+ issuedMs,
262
+ },
263
+ // === What to do next ===
264
+ usage: {
265
+ header: 'Authorization: Bearer <access_token>',
266
+ try_it: 'GET /agent-only',
267
+ x402_demo: 'GET /agent-only/x402',
268
+ full_docs: 'GET / with Authorization: Bearer <access_token>',
269
+ refresh: 'POST /v1/token/refresh with {"refresh_token":"<refresh_token>"}',
270
+ },
271
+ });
272
+ }
273
+ catch (error) {
274
+ console.error('x402 challenge route error:', error);
275
+ return c.json({
276
+ error: 'INTERNAL_ERROR',
277
+ message: 'Internal server error during x402 payment processing',
278
+ }, 500);
279
+ }
280
+ }
281
+ /**
282
+ * GET /agent-only/x402
283
+ *
284
+ * Demo endpoint: requires BOTH BOTCHA Bearer token + x402 micropayment.
285
+ * Reference implementation for "verified + paid" double-gated resources.
286
+ *
287
+ * Without token → 401 (get BOTCHA verified first)
288
+ * Without payment → 402 (pay $0.001 USDC)
289
+ * With both → 200 (access granted)
290
+ */
291
+ export async function agentOnlyX402Route(c) {
292
+ const resource = '/agent-only/x402';
293
+ const wallet = getWallet(c.env);
294
+ // ---- Step 1: BOTCHA Bearer token verification ----
295
+ const authHeader = c.req.header('authorization');
296
+ const token = extractBearerToken(authHeader);
297
+ if (!token) {
298
+ return c.json({
299
+ error: 'UNAUTHORIZED',
300
+ message: 'This resource requires BOTCHA verification AND an x402 micropayment.',
301
+ step1: {
302
+ description: 'First, get a BOTCHA verification token (free challenge or paid x402)',
303
+ challenge: 'GET /v1/token → POST /v1/token/verify → access_token',
304
+ paid: `GET /v1/x402/challenge with X-Payment header → access_token`,
305
+ },
306
+ step2: {
307
+ description: 'Then, pay the x402 resource fee with your X-Payment header',
308
+ amount: VERIFICATION_PRICE_HUMAN,
309
+ network: `Base (eip155:${BASE_CHAIN_ID})`,
310
+ },
311
+ }, 401);
312
+ }
313
+ const publicKey = getPublicKey(c.env);
314
+ const tokenResult = await verifyToken(token, c.env.JWT_SECRET, c.env, undefined, publicKey);
315
+ if (!tokenResult.valid) {
316
+ return c.json({
317
+ error: 'INVALID_TOKEN',
318
+ message: tokenResult.error || 'Bearer token is invalid or expired',
319
+ hint: 'Get a fresh token: GET /v1/token → POST /v1/token/verify',
320
+ }, 401);
321
+ }
322
+ // ---- Step 2: x402 payment verification ----
323
+ const paymentHeader = c.req.header('x-payment');
324
+ if (!paymentHeader) {
325
+ const descriptor = buildPaymentRequiredDescriptor(resource, {
326
+ description: `BOTCHA-verified agent resource. Costs ${VERIFICATION_PRICE_HUMAN} USDC per access. Your BOTCHA identity is already verified — now pay to access.`,
327
+ payTo: wallet,
328
+ mimeType: 'application/json',
329
+ });
330
+ return c.json({
331
+ error: 'PAYMENT_REQUIRED',
332
+ message: `You are BOTCHA-verified! Now pay ${VERIFICATION_PRICE_HUMAN} USDC to access this resource.`,
333
+ verified_as: {
334
+ type: tokenResult.payload?.type,
335
+ app_id: tokenResult.payload?.app_id,
336
+ solve_time_ms: tokenResult.payload?.solveTime,
337
+ },
338
+ x402: descriptor,
339
+ instructions: [
340
+ '1. Sign an ERC-3009 transferWithAuthorization (payTo: ' + wallet + ', amount: ' + VERIFICATION_PRICE_USDC_UNITS + ' USDC units)',
341
+ '2. Base64-encode the X402PaymentProof JSON',
342
+ '3. Retry this GET request with X-Payment: <base64-proof>',
343
+ ],
344
+ }, 402, {
345
+ 'X-Payment-Required': JSON.stringify(descriptor),
346
+ 'X-Payment-Scheme': 'exact',
347
+ });
348
+ }
349
+ const proof = parsePaymentHeader(paymentHeader);
350
+ if (!proof) {
351
+ return c.json({
352
+ error: 'INVALID_PAYMENT',
353
+ message: 'X-Payment header is not valid base64-encoded X402PaymentProof JSON',
354
+ }, 400);
355
+ }
356
+ const verification = await verifyX402Payment(proof, c.env.NONCES, {
357
+ requiredRecipient: wallet,
358
+ requiredAmount: VERIFICATION_PRICE_USDC_UNITS,
359
+ });
360
+ if (!verification.verified) {
361
+ return c.json({
362
+ error: 'PAYMENT_FAILED',
363
+ message: verification.error,
364
+ errorCode: verification.errorCode,
365
+ }, 402, {
366
+ 'X-Payment-Required': JSON.stringify(buildPaymentRequiredDescriptor(resource, { payTo: wallet })),
367
+ });
368
+ }
369
+ // ---- Both verified! ----
370
+ const appId = extractAppId(c) || tokenResult.payload?.app_id;
371
+ // Store payment record
372
+ const paymentRecord = {
373
+ payment_id: verification.txHash,
374
+ payer: verification.payer,
375
+ amount: verification.amount,
376
+ network: verification.network,
377
+ tx_hash: verification.txHash,
378
+ resource,
379
+ nonce: proof.payload.nonce,
380
+ botcha_app_id: appId,
381
+ verified_at: Math.floor(Date.now() / 1000),
382
+ status: 'verified',
383
+ };
384
+ await storePaymentRecord(c.env.NONCES, paymentRecord);
385
+ // Set x402 response header
386
+ c.header('X-Payment-Response', buildPaymentResponseHeader(verification));
387
+ const payload = tokenResult.payload;
388
+ return c.json({
389
+ success: true,
390
+ message: '🤖 Double verified! You are a BOTCHA-verified agent that paid via x402.',
391
+ access: 'GRANTED',
392
+ timestamp: new Date().toISOString(),
393
+ // Identity proof
394
+ botcha_identity: {
395
+ verified: true,
396
+ type: payload?.type,
397
+ app_id: payload?.app_id,
398
+ audience: payload?.aud,
399
+ solve_time_ms: payload?.solveTime,
400
+ issued_at: payload?.iat ? new Date(payload.iat * 1000).toISOString() : null,
401
+ },
402
+ // Payment proof
403
+ payment_proof: {
404
+ verified: true,
405
+ payer: verification.payer,
406
+ txHash: verification.txHash,
407
+ amount: verification.amount,
408
+ amountHuman: `${(parseInt(verification.amount || '0') / 1e6).toFixed(6)} USDC`,
409
+ network: verification.network,
410
+ },
411
+ // The secret resource content
412
+ secret: 'This payload is gated behind BOTCHA identity + x402 payment. Your agent cleared both gates. 🔐',
413
+ demo_data: {
414
+ description: 'Copy this pattern to gate any resource behind verified identity + micropayment',
415
+ middleware: [
416
+ '1. Verify BOTCHA Bearer token (POST /v1/token/validate)',
417
+ '2. Verify X-Payment header (POST /v1/x402/verify-payment)',
418
+ '3. Grant access if both pass',
419
+ ],
420
+ sdk_coming_soon: 'npm install @dupecom/botcha-x402',
421
+ },
422
+ });
423
+ }
424
+ /**
425
+ * POST /v1/x402/webhook
426
+ *
427
+ * Receive x402 settlement notifications from facilitators (Coinbase CDP, etc.)
428
+ *
429
+ * This endpoint:
430
+ * - Validates the webhook signature (HMAC-SHA256 of payload with BOTCHA_WEBHOOK_SECRET)
431
+ * - Updates payment records
432
+ * - Credits agent reputation on successful payment
433
+ * - Returns 200 to acknowledge receipt
434
+ *
435
+ * Expected payload: X402WebhookEvent
436
+ */
437
+ export async function x402WebhookRoute(c) {
438
+ try {
439
+ const rawBody = await c.req.text();
440
+ let event;
441
+ try {
442
+ event = JSON.parse(rawBody);
443
+ }
444
+ catch {
445
+ return c.json({ error: 'Invalid JSON payload' }, 400);
446
+ }
447
+ // Validate webhook signature if secret is configured
448
+ const webhookSecret = c.env.BOTCHA_WEBHOOK_SECRET;
449
+ const signatureHeader = c.req.header('x-botcha-signature') || c.req.header('x-webhook-signature');
450
+ if (webhookSecret) {
451
+ if (!signatureHeader) {
452
+ return c.json({ error: 'Missing webhook signature' }, 401);
453
+ }
454
+ const sigValid = await verifyWebhookSignature(rawBody, signatureHeader, webhookSecret);
455
+ if (!sigValid) {
456
+ return c.json({ error: 'Invalid webhook signature' }, 401);
457
+ }
458
+ }
459
+ // Validate required fields
460
+ if (!event.event_type || !event.payment_id || !event.tx_hash) {
461
+ return c.json({
462
+ error: 'Missing required fields: event_type, payment_id, tx_hash',
463
+ }, 400);
464
+ }
465
+ const validEventTypes = ['payment.settled', 'payment.failed', 'payment.refunded'];
466
+ if (!validEventTypes.includes(event.event_type)) {
467
+ return c.json({
468
+ error: `Unknown event_type: ${event.event_type}`,
469
+ valid_types: validEventTypes,
470
+ }, 400);
471
+ }
472
+ // Process the event
473
+ const result = await processWebhookEvent(event, c.env.NONCES, c.env.AGENTS, c.env.SESSIONS, webhookSecret);
474
+ if (!result.handled) {
475
+ // Still return 200 to prevent retries; just log the failure
476
+ console.warn('Webhook event not fully handled:', result.message);
477
+ }
478
+ return c.json({
479
+ received: true,
480
+ event_type: event.event_type,
481
+ payment_id: event.payment_id,
482
+ handled: result.handled,
483
+ message: result.message,
484
+ timestamp: new Date().toISOString(),
485
+ });
486
+ }
487
+ catch (error) {
488
+ console.error('x402 webhook error:', error);
489
+ // Return 200 to prevent retries on internal errors
490
+ return c.json({
491
+ received: true,
492
+ error: 'Internal processing error (logged)',
493
+ });
494
+ }
495
+ }
496
+ /**
497
+ * GET /v1/x402/info
498
+ *
499
+ * Public endpoint: returns x402 payment configuration for this BOTCHA instance.
500
+ * Agents can discover pricing, wallet, and supported networks.
501
+ */
502
+ export async function x402InfoRoute(c) {
503
+ const wallet = getWallet(c.env);
504
+ const baseUrl = new URL(c.req.url).origin;
505
+ return c.json({
506
+ name: 'BOTCHA x402 Payment Gateway',
507
+ version: '1.0',
508
+ description: 'Pay USDC on Base to receive a BOTCHA verified agent token, or to access x402-gated resources.',
509
+ pricing: {
510
+ verification_token: {
511
+ amount: VERIFICATION_PRICE_USDC_UNITS,
512
+ amountHuman: VERIFICATION_PRICE_HUMAN,
513
+ description: 'One BOTCHA access_token (1 hour validity)',
514
+ },
515
+ resource_access: {
516
+ amount: VERIFICATION_PRICE_USDC_UNITS,
517
+ amountHuman: VERIFICATION_PRICE_HUMAN,
518
+ description: 'One-time access to an x402-gated resource',
519
+ },
520
+ },
521
+ payment: {
522
+ scheme: 'exact',
523
+ network: `eip155:${BASE_CHAIN_ID}`,
524
+ networkName: 'Base',
525
+ payTo: wallet,
526
+ asset: USDC_BASE_ADDRESS,
527
+ assetSymbol: 'USDC',
528
+ assetDecimals: 6,
529
+ deadlineSeconds: PAYMENT_DEADLINE_SECONDS,
530
+ },
531
+ endpoints: {
532
+ challenge: `${baseUrl}/v1/x402/challenge`,
533
+ verify_payment: `${baseUrl}/v1/x402/verify-payment`,
534
+ demo: `${baseUrl}/agent-only/x402`,
535
+ webhook: `${baseUrl}/v1/x402/webhook`,
536
+ },
537
+ x402_compliance: {
538
+ scheme: 'exact',
539
+ request_header: 'X-Payment',
540
+ response_header: 'X-Payment-Required',
541
+ confirmation_header: 'X-Payment-Response',
542
+ spec: 'https://x402.org',
543
+ },
544
+ verification_mode: 'cryptographic-signature-check + nonce replay protection',
545
+ });
546
+ }
547
+ // ============ WEBHOOK SIGNATURE VERIFICATION ============
548
+ /**
549
+ * Verify HMAC-SHA256 webhook signature.
550
+ * Facilitators sign payloads as: HMAC-SHA256(secret, body)
551
+ */
552
+ async function verifyWebhookSignature(rawBody, signatureHeader, secret) {
553
+ try {
554
+ const encoder = new TextEncoder();
555
+ const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
556
+ // Strip "sha256=" prefix if present (GitHub webhook style)
557
+ const sigHex = signatureHeader.replace(/^sha256=/, '');
558
+ const sigBytes = hexToBytes(sigHex);
559
+ return await crypto.subtle.verify('HMAC', key, sigBytes, encoder.encode(rawBody));
560
+ }
561
+ catch {
562
+ return false;
563
+ }
564
+ }
565
+ function hexToBytes(hex) {
566
+ const bytes = new Uint8Array(hex.length / 2);
567
+ for (let i = 0; i < hex.length; i += 2) {
568
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
569
+ }
570
+ return bytes;
571
+ }
572
+ // ============ EXPORTS ============
573
+ export default {
574
+ verifyPaymentRoute,
575
+ x402ChallengeRoute,
576
+ agentOnlyX402Route,
577
+ x402WebhookRoute,
578
+ x402InfoRoute,
579
+ };