@dupecom/botcha-cloudflare 0.20.2 → 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.
- package/README.md +74 -9
- package/dist/agent-auth.d.ts +129 -0
- package/dist/agent-auth.d.ts.map +1 -0
- package/dist/agent-auth.js +210 -0
- package/dist/agents.d.ts +10 -0
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +51 -1
- package/dist/app-gate.d.ts +6 -0
- package/dist/app-gate.d.ts.map +1 -0
- package/dist/app-gate.js +69 -0
- package/dist/apps.d.ts +13 -4
- package/dist/apps.d.ts.map +1 -1
- package/dist/apps.js +30 -4
- package/dist/dashboard/account.d.ts +63 -0
- package/dist/dashboard/account.d.ts.map +1 -0
- package/dist/dashboard/account.js +488 -0
- package/dist/dashboard/api.js +15 -68
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +14 -14
- package/dist/dashboard/docs.d.ts.map +1 -1
- package/dist/dashboard/docs.js +146 -3
- package/dist/dashboard/layout.d.ts.map +1 -1
- package/dist/dashboard/layout.js +2 -2
- package/dist/dashboard/mcp-setup.d.ts +15 -0
- package/dist/dashboard/mcp-setup.d.ts.map +1 -0
- package/dist/dashboard/mcp-setup.js +391 -0
- package/dist/dashboard/showcase.d.ts +6 -10
- package/dist/dashboard/showcase.d.ts.map +1 -1
- package/dist/dashboard/showcase.js +67 -991
- package/dist/dashboard/whitepaper.d.ts.map +1 -1
- package/dist/dashboard/whitepaper.js +42 -4
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +660 -83
- package/dist/mcp.d.ts +20 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +1290 -0
- package/dist/oauth-agent.d.ts +130 -0
- package/dist/oauth-agent.d.ts.map +1 -0
- package/dist/oauth-agent.js +194 -0
- package/dist/static.d.ts +781 -5
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +790 -111
- package/dist/tap-a2a-routes.d.ts +355 -0
- package/dist/tap-a2a-routes.d.ts.map +1 -0
- package/dist/tap-a2a-routes.js +475 -0
- package/dist/tap-a2a.d.ts +199 -0
- package/dist/tap-a2a.d.ts.map +1 -0
- package/dist/tap-a2a.js +502 -0
- package/dist/tap-agents.d.ts +15 -0
- package/dist/tap-agents.d.ts.map +1 -1
- package/dist/tap-agents.js +31 -1
- package/dist/tap-ans-routes.d.ts +302 -0
- package/dist/tap-ans-routes.d.ts.map +1 -0
- package/dist/tap-ans-routes.js +535 -0
- package/dist/tap-ans.d.ts +241 -0
- package/dist/tap-ans.d.ts.map +1 -0
- package/dist/tap-ans.js +481 -0
- package/dist/tap-delegation-routes.d.ts.map +1 -1
- package/dist/tap-delegation-routes.js +11 -0
- package/dist/tap-did.d.ts +140 -0
- package/dist/tap-did.d.ts.map +1 -0
- package/dist/tap-did.js +262 -0
- package/dist/tap-oidca-routes.d.ts +383 -0
- package/dist/tap-oidca-routes.d.ts.map +1 -0
- package/dist/tap-oidca-routes.js +597 -0
- package/dist/tap-oidca.d.ts +288 -0
- package/dist/tap-oidca.d.ts.map +1 -0
- package/dist/tap-oidca.js +461 -0
- package/dist/tap-routes.d.ts +24 -8
- package/dist/tap-routes.d.ts.map +1 -1
- package/dist/tap-routes.js +169 -23
- package/dist/tap-vc-routes.d.ts +358 -0
- package/dist/tap-vc-routes.d.ts.map +1 -0
- package/dist/tap-vc-routes.js +367 -0
- package/dist/tap-vc.d.ts +125 -0
- package/dist/tap-vc.d.ts.map +1 -0
- package/dist/tap-vc.js +245 -0
- package/dist/tap-x402-routes.d.ts +89 -0
- package/dist/tap-x402-routes.d.ts.map +1 -0
- package/dist/tap-x402-routes.js +579 -0
- package/dist/tap-x402.d.ts +222 -0
- package/dist/tap-x402.d.ts.map +1 -0
- package/dist/tap-x402.js +546 -0
- package/dist/webhooks.d.ts +99 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +642 -0
- package/package.json +3 -1
package/dist/tap-x402.js
ADDED
|
@@ -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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"}
|