@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,546 @@
1
+ /**
2
+ * x402 Payment Gating — BOTCHA Revenue via HTTP 402
3
+ *
4
+ * Implements the x402 HTTP Payment Required protocol so BOTCHA-verified
5
+ * agents can natively access paid APIs, and BOTCHA earns per-verification.
6
+ *
7
+ * Spec: https://x402.org / https://github.com/coinbase/x402
8
+ *
9
+ * Flow:
10
+ * 1. Agent requests a protected resource
11
+ * 2. Server returns HTTP 402 + X-Payment-Required header (payment details)
12
+ * 3. Agent signs a ERC-3009 transferWithAuthorization and sends X-Payment header
13
+ * 4. Server verifies payment on-chain (or via facilitator) and issues BOTCHA token
14
+ *
15
+ * Key standard:
16
+ * - Scheme: "exact" — fixed USD amount, exact recipient
17
+ * - Network: "base" (Base mainnet, chain ID 8453)
18
+ * - Token: USDC on Base (6 decimals, 1000 units = $0.001)
19
+ * - Verification: ERC-3009 signature (EIP-712 structured data)
20
+ */
21
+ import { generateToken } from './auth.js';
22
+ import { secp256k1 } from '@noble/curves/secp256k1';
23
+ import { keccak_256 } from '@noble/hashes/sha3';
24
+ import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from '@noble/hashes/utils';
25
+ // ============ CONSTANTS ============
26
+ /**
27
+ * BOTCHA's receiving wallet on Base. Set via BOTCHA_PAYMENT_WALLET env var.
28
+ * Placeholder: in production, override with BOTCHA_PAYMENT_WALLET secret.
29
+ * This address (b07ca = "botcha" mapped to hex-safe chars) is not owned by anyone.
30
+ */
31
+ export const BOTCHA_WALLET = '0xb07ca00000000000000000000000000000000001';
32
+ /** USDC contract address on Base (mainnet) */
33
+ export const USDC_BASE_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
34
+ /** Base chain ID */
35
+ export const BASE_CHAIN_ID = 8453;
36
+ /** Price per BOTCHA verification in USDC atomic units (6 decimals) */
37
+ /** 1000 units = $0.001 USDC */
38
+ export const VERIFICATION_PRICE_USDC_UNITS = '1000';
39
+ /** Human-readable price */
40
+ export const VERIFICATION_PRICE_HUMAN = '$0.001 USDC';
41
+ /** Payment deadline window (seconds) */
42
+ export const PAYMENT_DEADLINE_SECONDS = 300; // 5 minutes
43
+ /** KV TTL for x402 nonces (replay protection) */
44
+ export const NONCE_TTL_SECONDS = 3600; // 1 hour
45
+ // ============ PAYMENT REQUIRED RESPONSE BUILDER ============
46
+ /**
47
+ * Build a standard x402 payment required descriptor.
48
+ * Goes into the X-Payment-Required response header.
49
+ */
50
+ export function buildPaymentRequiredDescriptor(resource, options) {
51
+ return {
52
+ scheme: 'exact',
53
+ network: `eip155:${BASE_CHAIN_ID}`, // CAIP-2 chain identifier
54
+ maxAmountRequired: options?.amount || VERIFICATION_PRICE_USDC_UNITS,
55
+ resource,
56
+ description: options?.description || `BOTCHA verification: ${VERIFICATION_PRICE_HUMAN} per verified agent token`,
57
+ mimeType: options?.mimeType || 'application/json',
58
+ payTo: options?.payTo || BOTCHA_WALLET,
59
+ maxTimeoutSeconds: PAYMENT_DEADLINE_SECONDS,
60
+ asset: USDC_BASE_ADDRESS,
61
+ extra: {
62
+ name: 'BOTCHA',
63
+ version: '1.0',
64
+ botcha_app_id: options?.appId,
65
+ },
66
+ };
67
+ }
68
+ /**
69
+ * Parse and decode an X-Payment header value (base64 JSON)
70
+ * Returns null if invalid format.
71
+ */
72
+ export function parsePaymentHeader(headerValue) {
73
+ try {
74
+ const decoded = atob(headerValue.trim());
75
+ const proof = JSON.parse(decoded);
76
+ // Minimal structural validation
77
+ if (!proof.scheme || proof.scheme !== 'exact')
78
+ return null;
79
+ if (!proof.network)
80
+ return null;
81
+ if (!proof.payload)
82
+ return null;
83
+ if (!proof.payload.from || !proof.payload.to)
84
+ return null;
85
+ if (!proof.payload.value)
86
+ return null;
87
+ if (!proof.payload.nonce)
88
+ return null;
89
+ if (!proof.payload.signature)
90
+ return null;
91
+ if (!proof.payload.validBefore)
92
+ return null;
93
+ return proof;
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ // ============ PAYMENT VERIFICATION ============
100
+ /**
101
+ * Verify an x402 payment proof.
102
+ *
103
+ * Checks:
104
+ * 1. Network matches Base
105
+ * 2. Recipient matches BOTCHA wallet
106
+ * 3. Amount >= required
107
+ * 4. Deadline has not expired (validBefore)
108
+ * 5. Nonce has not been replayed (KV check)
109
+ * 6. EIP-712 signature is valid (ERC-3009)
110
+ *
111
+ * Note: This verifies the ERC-3009 typed-data signature locally.
112
+ * It does not confirm on-chain settlement; use facilitator webhooks for that.
113
+ */
114
+ export async function verifyX402Payment(proof, noncesKV, options) {
115
+ try {
116
+ const payload = proof.payload;
117
+ const requiredTo = options?.requiredRecipient || BOTCHA_WALLET;
118
+ const requiredAmount = options?.requiredAmount || VERIFICATION_PRICE_USDC_UNITS;
119
+ // 1. Network check (accept eip155:8453 or "base")
120
+ const networkOk = proof.network === `eip155:${BASE_CHAIN_ID}` ||
121
+ proof.network === 'base' ||
122
+ proof.network === 'base-mainnet';
123
+ if (!networkOk) {
124
+ return {
125
+ verified: false,
126
+ valid: false,
127
+ error: `Unsupported network: ${proof.network}. Use Base (eip155:8453).`,
128
+ errorCode: 'NETWORK_MISMATCH',
129
+ };
130
+ }
131
+ // 2. Recipient check (case-insensitive hex comparison)
132
+ if (payload.to.toLowerCase() !== requiredTo.toLowerCase()) {
133
+ return {
134
+ verified: false,
135
+ valid: false,
136
+ error: `Payment recipient mismatch. Expected: ${requiredTo}, Got: ${payload.to}`,
137
+ errorCode: 'RECIPIENT_MISMATCH',
138
+ };
139
+ }
140
+ // 3. Amount check
141
+ const providedAmount = BigInt(payload.value);
142
+ const minAmount = BigInt(requiredAmount);
143
+ if (providedAmount < minAmount) {
144
+ return {
145
+ verified: false,
146
+ valid: false,
147
+ error: `Insufficient payment. Required: ${requiredAmount} USDC units, Got: ${payload.value}`,
148
+ errorCode: 'INSUFFICIENT_AMOUNT',
149
+ };
150
+ }
151
+ if (typeof payload.chainId === 'number' && payload.chainId !== BASE_CHAIN_ID) {
152
+ return {
153
+ verified: false,
154
+ valid: false,
155
+ error: `Unsupported chainId in payload: ${payload.chainId}. Use ${BASE_CHAIN_ID}.`,
156
+ errorCode: 'NETWORK_MISMATCH',
157
+ };
158
+ }
159
+ // 4. Time window checks
160
+ const now = Math.floor(Date.now() / 1000);
161
+ const validAfter = parseInt(payload.validAfter || '0', 10);
162
+ const validBefore = parseInt(payload.validBefore, 10);
163
+ if (!Number.isFinite(validAfter) || !Number.isFinite(validBefore) || validBefore <= validAfter) {
164
+ return {
165
+ verified: false,
166
+ valid: false,
167
+ error: 'Payment authorization has an invalid validity window',
168
+ errorCode: 'SIGNATURE_INVALID',
169
+ };
170
+ }
171
+ if (validAfter > now) {
172
+ return {
173
+ verified: false,
174
+ valid: false,
175
+ error: `Payment authorization is not active until ${new Date(validAfter * 1000).toISOString()}`,
176
+ errorCode: 'PAYMENT_NOT_YET_VALID',
177
+ };
178
+ }
179
+ if (validBefore <= now) {
180
+ return {
181
+ verified: false,
182
+ valid: false,
183
+ error: `Payment authorization expired at ${new Date(validBefore * 1000).toISOString()}`,
184
+ errorCode: 'PAYMENT_EXPIRED',
185
+ };
186
+ }
187
+ // 5. Replay protection: check nonce hasn't been used
188
+ const nonceKey = `x402_nonce:${payload.nonce.toLowerCase()}`;
189
+ const existingNonce = await noncesKV.get(nonceKey);
190
+ if (existingNonce) {
191
+ return {
192
+ verified: false,
193
+ valid: false,
194
+ error: 'Payment nonce already used (replay attack prevented)',
195
+ errorCode: 'NONCE_REPLAY',
196
+ };
197
+ }
198
+ // 6. EIP-712 / ERC-3009 signature verification
199
+ const sigValid = await verifyERC3009Signature(payload);
200
+ if (!sigValid) {
201
+ return {
202
+ verified: false,
203
+ valid: false,
204
+ error: 'ERC-3009 signature verification failed',
205
+ errorCode: 'SIGNATURE_INVALID',
206
+ };
207
+ }
208
+ // Mark nonce as used (TTL: 1 hour past validBefore)
209
+ const nonceTtl = Math.max(NONCE_TTL_SECONDS, validBefore - now + 60);
210
+ await noncesKV.put(nonceKey, '1', { expirationTtl: nonceTtl });
211
+ // Compute a deterministic tx hash from the signed payload (for record-keeping)
212
+ // In production this would be the actual on-chain tx hash from the facilitator
213
+ const txHash = await computePaymentId(payload);
214
+ return {
215
+ verified: true,
216
+ valid: true,
217
+ txHash,
218
+ payer: payload.from,
219
+ amount: payload.value,
220
+ network: proof.network,
221
+ };
222
+ }
223
+ catch (error) {
224
+ return {
225
+ verified: false,
226
+ valid: false,
227
+ error: `Payment verification error: ${error instanceof Error ? error.message : 'Unknown error'}`,
228
+ errorCode: 'INTERNAL_ERROR',
229
+ };
230
+ }
231
+ }
232
+ /**
233
+ * Verify ERC-3009 transferWithAuthorization EIP-712 signature.
234
+ *
235
+ * EIP-712 domain:
236
+ * name: "USD Coin" (USDC contract name on Base)
237
+ * version: "2"
238
+ * chainId: 8453
239
+ * verifyingContract: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
240
+ *
241
+ * Primary type: TransferWithAuthorization
242
+ * Types:
243
+ * TransferWithAuthorization: from, to, value, validAfter, validBefore, nonce
244
+ *
245
+ * Uses secp256k1 pubkey recovery over the EIP-712 digest and checks the
246
+ * recovered address matches payload.from.
247
+ */
248
+ async function verifyERC3009Signature(payload) {
249
+ try {
250
+ if (!isHexAddress(payload.from) || !isHexAddress(payload.to))
251
+ return false;
252
+ if (!/^\d+$/.test(payload.value))
253
+ return false;
254
+ if (!/^\d+$/.test(payload.validBefore))
255
+ return false;
256
+ if (!/^\d+$/.test(payload.validAfter || '0'))
257
+ return false;
258
+ if (BigInt(payload.value) <= 0n)
259
+ return false;
260
+ const sigHex = normalizeHex(payload.signature);
261
+ if (!payload.signature.toLowerCase().startsWith('0x'))
262
+ return false;
263
+ if (!/^[0-9a-fA-F]{130}$/.test(sigHex))
264
+ return false;
265
+ const sigBytes = hexToBytes(sigHex);
266
+ const recoveryBit = normalizeRecoveryBit(sigBytes[64]);
267
+ if (recoveryBit === null)
268
+ return false;
269
+ const digest = buildERC3009TransferDigest(payload);
270
+ const signature = secp256k1.Signature.fromCompact(sigBytes.slice(0, 64)).addRecoveryBit(recoveryBit);
271
+ const recovered = signature.recoverPublicKey(digest);
272
+ const recoveredAddress = publicKeyToAddress(recovered.toRawBytes(false));
273
+ return recoveredAddress.toLowerCase() === payload.from.toLowerCase();
274
+ }
275
+ catch {
276
+ return false;
277
+ }
278
+ }
279
+ /**
280
+ * Compute a deterministic payment ID from ERC-3009 payload.
281
+ * This is not an on-chain tx hash; it is an idempotent verifier-side ID.
282
+ */
283
+ async function computePaymentId(payload) {
284
+ const digest = buildERC3009TransferDigest(payload);
285
+ const signature = hexToBytes(normalizeHex(payload.signature));
286
+ return `0x${bytesToHex(keccak_256(concatBytes(digest, signature)))}`;
287
+ }
288
+ const UINT256_MAX = (1n << 256n) - 1n;
289
+ const EIP712_PREFIX = Uint8Array.from([0x19, 0x01]);
290
+ const EIP712_DOMAIN_TYPEHASH = keccak_256(utf8ToBytes('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'));
291
+ const ERC3009_TYPEHASH = keccak_256(utf8ToBytes('TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)'));
292
+ const USDC_NAME_HASH = keccak_256(utf8ToBytes('USD Coin'));
293
+ const USDC_VERSION_HASH = keccak_256(utf8ToBytes('2'));
294
+ function normalizeHex(value) {
295
+ return value.replace(/^0x/i, '');
296
+ }
297
+ function isHexAddress(value) {
298
+ return /^0x[0-9a-fA-F]{40}$/.test(value);
299
+ }
300
+ function normalizeRecoveryBit(v) {
301
+ if (v === 0 || v === 1)
302
+ return v;
303
+ if (v === 27 || v === 28)
304
+ return v - 27;
305
+ return null;
306
+ }
307
+ function toUint256Word(value) {
308
+ if (value < 0n || value > UINT256_MAX) {
309
+ throw new Error('uint256 value out of range');
310
+ }
311
+ return hexToBytes(value.toString(16).padStart(64, '0'));
312
+ }
313
+ function toAddressWord(address) {
314
+ if (!isHexAddress(address)) {
315
+ throw new Error('invalid address');
316
+ }
317
+ return concatBytes(new Uint8Array(12), hexToBytes(normalizeHex(address)));
318
+ }
319
+ function toBytes32Word(value) {
320
+ const hex = normalizeHex(value);
321
+ if (!/^[0-9a-fA-F]{64}$/.test(hex)) {
322
+ throw new Error('invalid bytes32');
323
+ }
324
+ return hexToBytes(hex);
325
+ }
326
+ function publicKeyToAddress(publicKey) {
327
+ const uncompressed = publicKey.length === 65 ? publicKey.slice(1) : publicKey;
328
+ const digest = keccak_256(uncompressed);
329
+ return `0x${bytesToHex(digest.slice(-20))}`;
330
+ }
331
+ export function buildERC3009TransferDigest(payload) {
332
+ const chainId = payload.chainId ?? BASE_CHAIN_ID;
333
+ if (!Number.isInteger(chainId) || chainId !== BASE_CHAIN_ID) {
334
+ throw new Error(`unsupported chainId: ${chainId}`);
335
+ }
336
+ if (!/^\d+$/.test(payload.value) || !/^\d+$/.test(payload.validAfter || '0') || !/^\d+$/.test(payload.validBefore)) {
337
+ throw new Error('invalid numeric fields');
338
+ }
339
+ const value = BigInt(payload.value);
340
+ const validAfter = BigInt(payload.validAfter || '0');
341
+ const validBefore = BigInt(payload.validBefore);
342
+ const domainSeparator = keccak_256(concatBytes(EIP712_DOMAIN_TYPEHASH, USDC_NAME_HASH, USDC_VERSION_HASH, toUint256Word(BigInt(chainId)), toAddressWord(USDC_BASE_ADDRESS)));
343
+ const structHash = keccak_256(concatBytes(ERC3009_TYPEHASH, toAddressWord(payload.from), toAddressWord(payload.to), toUint256Word(value), toUint256Word(validAfter), toUint256Word(validBefore), toBytes32Word(payload.nonce)));
344
+ return keccak_256(concatBytes(EIP712_PREFIX, domainSeparator, structHash));
345
+ }
346
+ // ============ PAYMENT RECORDS ============
347
+ /**
348
+ * Store a verified x402 payment record in KV
349
+ */
350
+ export async function storePaymentRecord(noncesKV, record) {
351
+ await noncesKV.put(`x402_payment:${record.payment_id}`, JSON.stringify(record), { expirationTtl: 86400 * 7 } // 7 days
352
+ );
353
+ // Index by payer address for history lookups
354
+ await noncesKV.put(`x402_payer_last:${record.payer.toLowerCase()}`, record.payment_id, { expirationTtl: 86400 * 7 });
355
+ }
356
+ /**
357
+ * Get a payment record by ID
358
+ */
359
+ export async function getPaymentRecord(noncesKV, paymentId) {
360
+ const data = await noncesKV.get(`x402_payment:${paymentId}`);
361
+ if (!data)
362
+ return null;
363
+ try {
364
+ return JSON.parse(data);
365
+ }
366
+ catch {
367
+ return null;
368
+ }
369
+ }
370
+ // ============ BOTCHA TOKEN ISSUANCE VIA PAYMENT ============
371
+ /**
372
+ * Issue a BOTCHA access_token in exchange for a verified x402 payment.
373
+ * This is the core pay-for-verification flow.
374
+ *
375
+ * The issued token is identical to a challenge-solve token —
376
+ * agents that pay get the same trust level as agents that solve.
377
+ */
378
+ export async function issueTokenForPayment(challengesKV, env, options, signingKey) {
379
+ // Use payment ID as the "challenge ID" in the token subject claim
380
+ const result = await generateToken(`x402:${options.paymentId}`, options.solveTimeMs || 0, env.JWT_SECRET, { CHALLENGES: challengesKV }, {
381
+ app_id: options.appId,
382
+ aud: options.audience || 'botcha-x402',
383
+ }, signingKey);
384
+ return result;
385
+ }
386
+ // ============ X402 PAYMENT RESPONSE HEADER BUILDER ============
387
+ /**
388
+ * Build the X-Payment-Response header value for successful payment
389
+ */
390
+ export function buildPaymentResponseHeader(result) {
391
+ return JSON.stringify({
392
+ success: result.verified,
393
+ txHash: result.txHash,
394
+ networkId: result.network || `eip155:${BASE_CHAIN_ID}`,
395
+ });
396
+ }
397
+ // ============ WEBHOOK PROCESSING ============
398
+ /**
399
+ * Process an inbound x402 webhook event from a facilitator.
400
+ *
401
+ * On payment.settled: update agent reputation, store payment record.
402
+ * On payment.failed: log failure.
403
+ * On payment.refunded: note in record.
404
+ */
405
+ export async function processWebhookEvent(event, noncesKV, agentsKV, sessionsKV, webhookSecret) {
406
+ try {
407
+ switch (event.event_type) {
408
+ case 'payment.settled': {
409
+ // Update or create payment record
410
+ const existing = await getPaymentRecord(noncesKV, event.payment_id);
411
+ if (existing) {
412
+ existing.status = 'verified';
413
+ await storePaymentRecord(noncesKV, existing);
414
+ }
415
+ // Record positive reputation event for the payer if we can map them to an agent
416
+ if (event.metadata?.agent_id) {
417
+ await recordPaymentReputationEvent(sessionsKV, {
418
+ agent_id: event.metadata.agent_id,
419
+ app_id: event.metadata.app_id || 'unknown',
420
+ action: 'auth_success',
421
+ metadata: {
422
+ tx_hash: event.tx_hash,
423
+ amount: event.amount,
424
+ event_type: 'x402_payment_settled',
425
+ },
426
+ });
427
+ }
428
+ return { handled: true, message: `Payment settled: ${event.tx_hash}` };
429
+ }
430
+ case 'payment.failed': {
431
+ const existing = await getPaymentRecord(noncesKV, event.payment_id);
432
+ if (existing) {
433
+ existing.status = 'rejected';
434
+ await storePaymentRecord(noncesKV, existing);
435
+ }
436
+ // Record negative reputation event
437
+ if (event.metadata?.agent_id) {
438
+ await recordPaymentReputationEvent(sessionsKV, {
439
+ agent_id: event.metadata.agent_id,
440
+ app_id: event.metadata.app_id || 'unknown',
441
+ action: 'auth_failure',
442
+ metadata: {
443
+ tx_hash: event.tx_hash,
444
+ event_type: 'x402_payment_failed',
445
+ },
446
+ });
447
+ }
448
+ return { handled: true, message: `Payment failed: ${event.payment_id}` };
449
+ }
450
+ case 'payment.refunded': {
451
+ const existing = await getPaymentRecord(noncesKV, event.payment_id);
452
+ if (existing) {
453
+ await storePaymentRecord(noncesKV, { ...existing, status: 'rejected' });
454
+ }
455
+ return { handled: true, message: `Payment refunded: ${event.payment_id}` };
456
+ }
457
+ default: {
458
+ return { handled: false, message: `Unknown event type: ${event.event_type}` };
459
+ }
460
+ }
461
+ }
462
+ catch (error) {
463
+ return {
464
+ handled: false,
465
+ message: `Webhook processing error: ${error instanceof Error ? error.message : 'Unknown error'}`,
466
+ };
467
+ }
468
+ }
469
+ /**
470
+ * Record a reputation event for an x402 payment action.
471
+ * Uses the existing TAP reputation system (verification category).
472
+ */
473
+ async function recordPaymentReputationEvent(sessionsKV, options) {
474
+ try {
475
+ // Load existing reputation score
476
+ const key = `reputation:${options.agent_id}`;
477
+ const raw = await sessionsKV.get(key);
478
+ const BASE_SCORE = 500;
479
+ const DELTAS = {
480
+ auth_success: 10,
481
+ challenge_solved: 15,
482
+ auth_failure: -20,
483
+ };
484
+ const delta = DELTAS[options.action] || 0;
485
+ const now = Date.now();
486
+ let score = raw ? JSON.parse(raw) : null;
487
+ if (!score) {
488
+ score = {
489
+ agent_id: options.agent_id,
490
+ app_id: options.app_id,
491
+ score: BASE_SCORE,
492
+ tier: 'neutral',
493
+ event_count: 0,
494
+ positive_events: 0,
495
+ negative_events: 0,
496
+ last_event_at: null,
497
+ created_at: now,
498
+ updated_at: now,
499
+ category_scores: {
500
+ verification: 0,
501
+ attestation: 0,
502
+ delegation: 0,
503
+ session: 0,
504
+ violation: 0,
505
+ endorsement: 0,
506
+ },
507
+ };
508
+ }
509
+ score.score = Math.max(0, Math.min(1000, score.score + delta));
510
+ score.event_count += 1;
511
+ if (delta >= 0)
512
+ score.positive_events += 1;
513
+ else
514
+ score.negative_events += 1;
515
+ score.last_event_at = now;
516
+ score.updated_at = now;
517
+ score.category_scores.verification = (score.category_scores.verification || 0) + delta;
518
+ score.tier = computeTier(score.score);
519
+ await sessionsKV.put(key, JSON.stringify(score));
520
+ }
521
+ catch (error) {
522
+ console.error('Failed to record payment reputation event:', error);
523
+ }
524
+ }
525
+ function computeTier(score) {
526
+ if (score < 200)
527
+ return 'untrusted';
528
+ if (score < 400)
529
+ return 'low';
530
+ if (score < 600)
531
+ return 'neutral';
532
+ if (score < 800)
533
+ return 'good';
534
+ return 'excellent';
535
+ }
536
+ // ============ EXPORTS ============
537
+ export default {
538
+ buildPaymentRequiredDescriptor,
539
+ parsePaymentHeader,
540
+ verifyX402Payment,
541
+ issueTokenForPayment,
542
+ buildPaymentResponseHeader,
543
+ storePaymentRecord,
544
+ getPaymentRecord,
545
+ processWebhookEvent,
546
+ };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * BOTCHA Webhook Event System
3
+ *
4
+ * Delivers real-time events to API owners when things happen:
5
+ * token.created / token.revoked
6
+ * agent.tap.registered / tap.session.created
7
+ * delegation.created / delegation.revoked
8
+ *
9
+ * KV keys (all stored in AGENTS namespace):
10
+ * webhook:{id} — WebhookConfig (without secret)
11
+ * webhook_secret:{id} — HMAC signing secret
12
+ * app_webhooks:{app_id} — JSON string[] of webhook IDs for this app
13
+ * webhook_deliveries:{id} — JSON DeliveryLog[] (last 100, TTL 7d)
14
+ */
15
+ import type { Context } from 'hono';
16
+ /** KV namespace interface — mirrors Cloudflare's KVNamespace */
17
+ export interface KVNamespace {
18
+ get(key: string, type?: 'text'): Promise<string | null>;
19
+ put(key: string, value: string, options?: {
20
+ expirationTtl?: number;
21
+ }): Promise<void>;
22
+ delete(key: string): Promise<void>;
23
+ }
24
+ export type WebhookEventType = 'agent.tap.registered' | 'token.created' | 'token.revoked' | 'tap.session.created' | 'delegation.created' | 'delegation.revoked';
25
+ export declare const ALL_EVENT_TYPES: WebhookEventType[];
26
+ export interface WebhookConfig {
27
+ id: string;
28
+ app_id: string;
29
+ url: string;
30
+ events: WebhookEventType[];
31
+ enabled: boolean;
32
+ created_at: number;
33
+ updated_at: number;
34
+ /** Consecutive failures since last success */
35
+ consecutive_failures: number;
36
+ /** Suspended after 3+ consecutive failures over 24h */
37
+ suspended: boolean;
38
+ }
39
+ export interface DeliveryLog {
40
+ delivery_id: string;
41
+ webhook_id: string;
42
+ event_type: string;
43
+ event_id: string;
44
+ attempted_at: number;
45
+ attempt_number: number;
46
+ status_code: number | null;
47
+ success: boolean;
48
+ error?: string;
49
+ duration_ms: number;
50
+ }
51
+ export interface BotchaEvent {
52
+ id: string;
53
+ type: WebhookEventType;
54
+ created_at: string;
55
+ app_id: string;
56
+ data: Record<string, unknown>;
57
+ }
58
+ export declare function computeHmacSignature(secret: string, body: string): Promise<string>;
59
+ /**
60
+ * Fire-and-forget webhook delivery with retry logic.
61
+ * Call via: ctx.waitUntil(triggerWebhook(...))
62
+ */
63
+ export declare function triggerWebhook(kv: KVNamespace, appId: string, eventType: WebhookEventType, data: Record<string, unknown>): Promise<void>;
64
+ /**
65
+ * POST /v1/webhooks
66
+ * Register a new webhook endpoint.
67
+ */
68
+ export declare function createWebhookRoute(c: Context): Promise<Response>;
69
+ /**
70
+ * GET /v1/webhooks
71
+ * List all webhooks for the authenticated app.
72
+ */
73
+ export declare function listWebhooksRoute(c: Context): Promise<Response>;
74
+ /**
75
+ * GET /v1/webhooks/:id
76
+ * Get a specific webhook.
77
+ */
78
+ export declare function getWebhookRoute(c: Context): Promise<Response>;
79
+ /**
80
+ * PUT /v1/webhooks/:id
81
+ * Update webhook (url, events, enabled).
82
+ */
83
+ export declare function updateWebhookRoute(c: Context): Promise<Response>;
84
+ /**
85
+ * DELETE /v1/webhooks/:id
86
+ * Delete a webhook.
87
+ */
88
+ export declare function deleteWebhookRoute(c: Context): Promise<Response>;
89
+ /**
90
+ * POST /v1/webhooks/:id/test
91
+ * Send a test event to verify endpoint reachability.
92
+ */
93
+ export declare function testWebhookRoute(c: Context): Promise<Response>;
94
+ /**
95
+ * GET /v1/webhooks/:id/deliveries
96
+ * Get recent delivery log (last 100 attempts).
97
+ */
98
+ export declare function listDeliveriesRoute(c: Context): Promise<Response>;
99
+ //# sourceMappingURL=webhooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"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"}