@dupecom/botcha-cloudflare 0.15.0 → 0.18.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 (65) hide show
  1. package/dist/dashboard/landing.d.ts.map +1 -1
  2. package/dist/dashboard/landing.js +2 -9
  3. package/dist/dashboard/layout.d.ts +12 -0
  4. package/dist/dashboard/layout.d.ts.map +1 -1
  5. package/dist/dashboard/layout.js +12 -5
  6. package/dist/dashboard/showcase.d.ts +1 -0
  7. package/dist/dashboard/showcase.d.ts.map +1 -1
  8. package/dist/dashboard/showcase.js +3 -2
  9. package/dist/dashboard/whitepaper.d.ts +14 -0
  10. package/dist/dashboard/whitepaper.d.ts.map +1 -0
  11. package/dist/dashboard/whitepaper.js +418 -0
  12. package/dist/email.d.ts.map +1 -1
  13. package/dist/email.js +5 -1
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +148 -18
  17. package/dist/og-image.d.ts +2 -0
  18. package/dist/og-image.d.ts.map +1 -0
  19. package/dist/og-image.js +2 -0
  20. package/dist/static.d.ts +871 -2
  21. package/dist/static.d.ts.map +1 -1
  22. package/dist/static.js +812 -4
  23. package/dist/tap-agents.d.ts +3 -2
  24. package/dist/tap-agents.d.ts.map +1 -1
  25. package/dist/tap-agents.js +19 -6
  26. package/dist/tap-attestation-routes.d.ts +204 -0
  27. package/dist/tap-attestation-routes.d.ts.map +1 -0
  28. package/dist/tap-attestation-routes.js +396 -0
  29. package/dist/tap-attestation.d.ts +178 -0
  30. package/dist/tap-attestation.d.ts.map +1 -0
  31. package/dist/tap-attestation.js +416 -0
  32. package/dist/tap-consumer.d.ts +151 -0
  33. package/dist/tap-consumer.d.ts.map +1 -0
  34. package/dist/tap-consumer.js +346 -0
  35. package/dist/tap-delegation-routes.d.ts +236 -0
  36. package/dist/tap-delegation-routes.d.ts.map +1 -0
  37. package/dist/tap-delegation-routes.js +378 -0
  38. package/dist/tap-delegation.d.ts +127 -0
  39. package/dist/tap-delegation.d.ts.map +1 -0
  40. package/dist/tap-delegation.js +490 -0
  41. package/dist/tap-edge.d.ts +106 -0
  42. package/dist/tap-edge.d.ts.map +1 -0
  43. package/dist/tap-edge.js +487 -0
  44. package/dist/tap-federation.d.ts +89 -0
  45. package/dist/tap-federation.d.ts.map +1 -0
  46. package/dist/tap-federation.js +237 -0
  47. package/dist/tap-jwks.d.ts +64 -0
  48. package/dist/tap-jwks.d.ts.map +1 -0
  49. package/dist/tap-jwks.js +279 -0
  50. package/dist/tap-payment.d.ts +172 -0
  51. package/dist/tap-payment.d.ts.map +1 -0
  52. package/dist/tap-payment.js +425 -0
  53. package/dist/tap-reputation-routes.d.ts +154 -0
  54. package/dist/tap-reputation-routes.d.ts.map +1 -0
  55. package/dist/tap-reputation-routes.js +341 -0
  56. package/dist/tap-reputation.d.ts +136 -0
  57. package/dist/tap-reputation.d.ts.map +1 -0
  58. package/dist/tap-reputation.js +346 -0
  59. package/dist/tap-routes.d.ts +239 -2
  60. package/dist/tap-routes.d.ts.map +1 -1
  61. package/dist/tap-routes.js +279 -4
  62. package/dist/tap-verify.d.ts +43 -1
  63. package/dist/tap-verify.d.ts.map +1 -1
  64. package/dist/tap-verify.js +215 -30
  65. package/package.json +1 -1
@@ -4,13 +4,20 @@
4
4
  *
5
5
  * Integrates with existing BOTCHA verification middleware to provide
6
6
  * enterprise-grade cryptographic agent authentication
7
+ *
8
+ * FEATURES:
9
+ * - Ed25519, ECDSA P-256, RSA-PSS signature verification
10
+ * - Full RFC 9421 compliance (sig1/sig2 labels, expires, nonce, tag)
11
+ * - Nonce replay protection via KV
12
+ * - TAP timestamp validation (created, expires, 8-minute max window)
13
+ * - Backward compatible with existing BOTCHA agents
7
14
  */
8
15
  import { TAP_VALID_ACTIONS } from './tap-agents.js';
9
16
  // ============ HTTP MESSAGE SIGNATURES (RFC 9421) ============
10
17
  /**
11
18
  * Verify HTTP Message Signature according to RFC 9421
12
19
  */
13
- export async function verifyHTTPMessageSignature(request, publicKey, algorithm) {
20
+ export async function verifyHTTPMessageSignature(request, publicKey, algorithm, nonces) {
14
21
  try {
15
22
  const { headers } = request;
16
23
  const signature = headers['signature'];
@@ -23,19 +30,31 @@ export async function verifyHTTPMessageSignature(request, publicKey, algorithm)
23
30
  if (!parsed) {
24
31
  return { valid: false, error: 'Invalid signature-input format' };
25
32
  }
26
- // Check timestamp (prevent replay attacks)
27
- if (parsed.created) {
28
- const now = Math.floor(Date.now() / 1000);
29
- const maxAge = 300; // 5 minutes
30
- if (Math.abs(now - parsed.created) > maxAge) {
31
- return { valid: false, error: 'Signature timestamp too old or too new' };
33
+ // Validate timestamps
34
+ const timestampValidation = validateTimestamps(parsed.created, parsed.expires);
35
+ if (!timestampValidation.valid) {
36
+ return { valid: false, error: timestampValidation.error };
37
+ }
38
+ // Check nonce replay (if nonce provided and KV available)
39
+ if (parsed.nonce && nonces) {
40
+ const nonceCheck = await checkAndStoreNonce(nonces, parsed.nonce);
41
+ if (nonceCheck.replay) {
42
+ return { valid: false, error: 'Nonce replay detected' };
32
43
  }
33
44
  }
34
45
  // Build signature base
35
- const signatureBase = buildSignatureBase(request.method, request.path, headers, parsed.components, parsed.created, parsed.keyId, parsed.algorithm);
46
+ const signatureBase = buildSignatureBase(request.method, request.path, headers, parsed);
36
47
  // Verify signature
37
- const isValid = await verifyCryptoSignature(signatureBase, signature, publicKey, algorithm);
38
- return { valid: isValid, error: isValid ? undefined : 'Signature verification failed' };
48
+ const isValid = await verifyCryptoSignature(signatureBase, signature, publicKey, algorithm, parsed.label);
49
+ return {
50
+ valid: isValid,
51
+ error: isValid ? undefined : 'Signature verification failed',
52
+ metadata: {
53
+ nonce: parsed.nonce,
54
+ tag: parsed.tag,
55
+ key_id: parsed.keyId
56
+ }
57
+ };
39
58
  }
40
59
  catch (error) {
41
60
  return { valid: false, error: `Verification error: ${error instanceof Error ? error.message : 'Unknown error'}` };
@@ -43,25 +62,35 @@ export async function verifyHTTPMessageSignature(request, publicKey, algorithm)
43
62
  }
44
63
  /**
45
64
  * Parse signature-input header according to RFC 9421
65
+ * Supports BOTH sig1 (BOTCHA) and sig2 (Visa TAP) labels
46
66
  */
47
67
  function parseSignatureInput(input) {
48
68
  try {
49
- // Parse: sig1=("@method" "@path" "x-tap-agent-id");keyid="agent-123";alg="ecdsa-p256-sha256";created=1234567890
50
- const sigMatch = input.match(/sig1=\(([^)]+)\)/);
69
+ // Match sig1 OR sig2
70
+ const sigMatch = input.match(/(sig[12])=\(([^)]+)\)/);
51
71
  if (!sigMatch)
52
72
  return null;
53
- const components = sigMatch[1]
73
+ const label = sigMatch[1];
74
+ const components = sigMatch[2]
54
75
  .split(' ')
55
76
  .map(h => h.replace(/"/g, ''));
56
- const keyIdMatch = input.match(/keyid="([^"]+)"/);
77
+ // Extract all params (keyid/keyId, alg, created, expires, nonce, tag)
78
+ const keyIdMatch = input.match(/keyid="([^"]+)"/i);
57
79
  const algMatch = input.match(/alg="([^"]+)"/);
58
80
  const createdMatch = input.match(/created=(\d+)/);
81
+ const expiresMatch = input.match(/expires=(\d+)/);
82
+ const nonceMatch = input.match(/nonce="([^"]+)"/);
83
+ const tagMatch = input.match(/tag="([^"]+)"/);
59
84
  if (!keyIdMatch || !algMatch || !createdMatch)
60
85
  return null;
61
86
  return {
87
+ label,
62
88
  keyId: keyIdMatch[1],
63
89
  algorithm: algMatch[1],
64
90
  created: parseInt(createdMatch[1]),
91
+ expires: expiresMatch ? parseInt(expiresMatch[1]) : undefined,
92
+ nonce: nonceMatch ? nonceMatch[1] : undefined,
93
+ tag: tagMatch ? tagMatch[1] : undefined,
65
94
  components
66
95
  };
67
96
  }
@@ -70,11 +99,51 @@ function parseSignatureInput(input) {
70
99
  }
71
100
  }
72
101
  /**
73
- * Build signature base string according to RFC 9421
102
+ * Validate created/expires timestamps according to TAP spec
74
103
  */
75
- function buildSignatureBase(method, path, headers, components, created, keyId, algorithm) {
104
+ function validateTimestamps(created, expires) {
105
+ const now = Math.floor(Date.now() / 1000);
106
+ const clockSkew = 30; // 30 seconds tolerance for clock drift
107
+ // created must be in the past (with clock skew)
108
+ if (created > now + clockSkew) {
109
+ return { valid: false, error: 'Signature timestamp is in the future' };
110
+ }
111
+ // If expires is present, validate it
112
+ if (expires !== undefined) {
113
+ // expires must be in the future
114
+ if (expires < now) {
115
+ return { valid: false, error: 'Signature has expired' };
116
+ }
117
+ // expires - created must be <= 480 seconds (8 minutes per TAP spec)
118
+ const window = expires - created;
119
+ if (window > 480) {
120
+ return { valid: false, error: 'Signature validity window exceeds 8 minutes' };
121
+ }
122
+ }
123
+ else {
124
+ // No expires - fall back to 5-minute tolerance on created (backward compat)
125
+ const age = now - created;
126
+ if (age > 300) {
127
+ return { valid: false, error: 'Signature timestamp too old or too new' };
128
+ }
129
+ if (age < -clockSkew) {
130
+ return { valid: false, error: 'Signature timestamp too old or too new' };
131
+ }
132
+ }
133
+ return { valid: true };
134
+ }
135
+ /**
136
+ * Build signature base string according to RFC 9421 TAP format
137
+ *
138
+ * Format:
139
+ * "@authority": example.com
140
+ * "@path": /example-product
141
+ * "@signature-params": sig2=("@authority" "@path");created=1735689600;keyid="poqk...";alg="Ed25519";expires=1735693200;nonce="e8N7...";tag="agent-browser-auth"
142
+ */
143
+ function buildSignatureBase(method, path, headers, parsed) {
76
144
  const lines = [];
77
- for (const component of components) {
145
+ // Add component lines (values are bare, no quotes)
146
+ for (const component of parsed.components) {
78
147
  if (component === '@method') {
79
148
  lines.push(`"@method": ${method.toUpperCase()}`);
80
149
  }
@@ -82,7 +151,8 @@ function buildSignatureBase(method, path, headers, components, created, keyId, a
82
151
  lines.push(`"@path": ${path}`);
83
152
  }
84
153
  else if (component === '@authority') {
85
- lines.push(`"@authority": ${headers['host'] || ''}`);
154
+ const authority = headers['host'] || headers[':authority'] || '';
155
+ lines.push(`"@authority": ${authority}`);
86
156
  }
87
157
  else {
88
158
  const value = headers[component];
@@ -91,23 +161,34 @@ function buildSignatureBase(method, path, headers, components, created, keyId, a
91
161
  }
92
162
  }
93
163
  }
94
- // Add signature parameters
95
- const componentsList = components.map(c => `"${c}"`).join(' ');
96
- lines.push(`"@signature-params": (${componentsList});keyid="${keyId}";alg="${algorithm}";created=${created}`);
164
+ // Build @signature-params line with ALL fields
165
+ const componentsList = parsed.components.map(c => `"${c}"`).join(' ');
166
+ let paramsLine = `"@signature-params": ${parsed.label}=(${componentsList});created=${parsed.created};keyid="${parsed.keyId}";alg="${parsed.algorithm}"`;
167
+ if (parsed.expires !== undefined) {
168
+ paramsLine += `;expires=${parsed.expires}`;
169
+ }
170
+ if (parsed.nonce) {
171
+ paramsLine += `;nonce="${parsed.nonce}"`;
172
+ }
173
+ if (parsed.tag) {
174
+ paramsLine += `;tag="${parsed.tag}"`;
175
+ }
176
+ lines.push(paramsLine);
97
177
  return lines.join('\n');
98
178
  }
99
179
  /**
100
180
  * Verify cryptographic signature using Web Crypto API
101
181
  */
102
- async function verifyCryptoSignature(signatureBase, signature, publicKeyPem, algorithm) {
182
+ async function verifyCryptoSignature(signatureBase, signature, publicKeyPem, algorithm, label) {
103
183
  try {
104
- // Extract signature bytes
105
- const sigMatch = signature.match(/sig1=:([^:]+):/);
184
+ // Extract signature bytes using the correct label
185
+ const sigPattern = new RegExp(`${label}=:([^:]+):`);
186
+ const sigMatch = signature.match(sigPattern);
106
187
  if (!sigMatch)
107
188
  return false;
108
189
  const signatureBytes = Uint8Array.from(atob(sigMatch[1]), c => c.charCodeAt(0));
109
- // Import public key
110
- const keyData = pemToArrayBuffer(publicKeyPem);
190
+ // Import public key (handles both PEM and raw Ed25519)
191
+ const keyData = importPublicKey(publicKeyPem, algorithm);
111
192
  const cryptoKey = await crypto.subtle.importKey('spki', keyData, getImportParams(algorithm), false, ['verify']);
112
193
  // Verify signature
113
194
  const encoder = new TextEncoder();
@@ -119,6 +200,53 @@ async function verifyCryptoSignature(signatureBase, signature, publicKeyPem, alg
119
200
  return false;
120
201
  }
121
202
  }
203
+ /**
204
+ * Import public key - handles PEM SPKI and raw Ed25519 formats
205
+ */
206
+ function importPublicKey(key, algorithm) {
207
+ // Check if it's a raw Ed25519 key (32 bytes base64)
208
+ if (algorithm.toLowerCase().includes('ed25519') || algorithm === 'Ed25519') {
209
+ if (isRawEd25519Key(key)) {
210
+ return rawEd25519ToSPKI(key);
211
+ }
212
+ }
213
+ // Otherwise parse as PEM
214
+ return pemToArrayBuffer(key);
215
+ }
216
+ /**
217
+ * Detect raw Ed25519 public key (32 bytes = 43-44 base64 chars)
218
+ */
219
+ function isRawEd25519Key(key) {
220
+ const stripped = key.replace(/[\s\n\r-]/g, '').replace(/BEGIN.*?END[^-]*-*/g, '');
221
+ try {
222
+ const decoded = atob(stripped.replace(/-/g, '+').replace(/_/g, '/'));
223
+ return decoded.length === 32;
224
+ }
225
+ catch {
226
+ return false;
227
+ }
228
+ }
229
+ /**
230
+ * Convert raw 32-byte Ed25519 key to SPKI format
231
+ * SPKI format: ASN.1 header + 32-byte public key
232
+ */
233
+ function rawEd25519ToSPKI(rawKey) {
234
+ const rawBytes = Uint8Array.from(atob(rawKey), c => c.charCodeAt(0));
235
+ if (rawBytes.length !== 32) {
236
+ throw new Error('Invalid Ed25519 key length');
237
+ }
238
+ // SPKI header for Ed25519 (12 bytes)
239
+ const spkiHeader = new Uint8Array([
240
+ 0x30, 0x2a, // SEQUENCE (42 bytes)
241
+ 0x30, 0x05, // SEQUENCE (5 bytes) - algorithm
242
+ 0x06, 0x03, 0x2b, 0x65, 0x70, // OID 1.3.101.112 (Ed25519)
243
+ 0x03, 0x21, 0x00 // BIT STRING (33 bytes, 0 unused bits)
244
+ ]);
245
+ const spki = new Uint8Array(spkiHeader.length + rawBytes.length);
246
+ spki.set(spkiHeader, 0);
247
+ spki.set(rawBytes, spkiHeader.length);
248
+ return spki.buffer;
249
+ }
122
250
  /**
123
251
  * Convert PEM public key to ArrayBuffer
124
252
  */
@@ -138,6 +266,10 @@ function pemToArrayBuffer(pem) {
138
266
  * Get Web Crypto API algorithm parameters for key import
139
267
  */
140
268
  function getImportParams(algorithm) {
269
+ const alg = algorithm.toLowerCase();
270
+ if (alg.includes('ed25519')) {
271
+ return { name: 'Ed25519' }; // No hash or curve needed
272
+ }
141
273
  switch (algorithm) {
142
274
  case 'ecdsa-p256-sha256':
143
275
  return { name: 'ECDSA', namedCurve: 'P-256' };
@@ -151,6 +283,10 @@ function getImportParams(algorithm) {
151
283
  * Get Web Crypto API algorithm parameters for signature verification
152
284
  */
153
285
  function getVerifyParams(algorithm) {
286
+ const alg = algorithm.toLowerCase();
287
+ if (alg.includes('ed25519')) {
288
+ return { name: 'Ed25519' }; // No hash needed
289
+ }
154
290
  switch (algorithm) {
155
291
  case 'ecdsa-p256-sha256':
156
292
  return { name: 'ECDSA', hash: 'SHA-256' };
@@ -160,6 +296,36 @@ function getVerifyParams(algorithm) {
160
296
  throw new Error(`Unsupported algorithm: ${algorithm}`);
161
297
  }
162
298
  }
299
+ // ============ NONCE REPLAY PROTECTION ============
300
+ /**
301
+ * Check if nonce was already used and store it if new
302
+ * Returns { replay: true } if nonce was seen before
303
+ */
304
+ export async function checkAndStoreNonce(nonces, nonce) {
305
+ if (!nonces || !nonce)
306
+ return { replay: false };
307
+ try {
308
+ // Hash nonce for fixed-length KV key
309
+ const encoder = new TextEncoder();
310
+ const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(nonce));
311
+ const hashHex = Array.from(new Uint8Array(hashBuffer))
312
+ .map(b => b.toString(16).padStart(2, '0'))
313
+ .join('');
314
+ const key = `nonce:${hashHex}`;
315
+ // Check if already seen
316
+ const existing = await nonces.get(key);
317
+ if (existing)
318
+ return { replay: true };
319
+ // Store with 8-minute TTL (480 seconds per TAP spec)
320
+ await nonces.put(key, '1', { expirationTtl: 480 });
321
+ return { replay: false };
322
+ }
323
+ catch (error) {
324
+ console.error('Nonce check error:', error);
325
+ // Fail-open on KV errors (don't block legitimate requests)
326
+ return { replay: false };
327
+ }
328
+ }
163
329
  // ============ TAP INTENT VALIDATION ============
164
330
  /**
165
331
  * Parse and validate TAP intent from headers
@@ -190,6 +356,7 @@ export function parseTAPIntent(intentString) {
190
356
  // ============ TAP HEADER EXTRACTION ============
191
357
  /**
192
358
  * Extract TAP-specific headers from request
359
+ * Supports BOTH standard TAP (sig2 + tag) and BOTCHA extended (x-tap-* headers)
193
360
  */
194
361
  export function extractTAPHeaders(headers) {
195
362
  const tapHeaders = {
@@ -200,11 +367,18 @@ export function extractTAPHeaders(headers) {
200
367
  'signature': headers['signature'],
201
368
  'signature-input': headers['signature-input']
202
369
  };
203
- const hasTAPHeaders = Boolean(tapHeaders['x-tap-agent-id'] &&
370
+ // Check for standard TAP (sig2 + agent tag)
371
+ const signatureInput = headers['signature-input'] || '';
372
+ const hasAgentTag = /tag="agent-(browser|payer)-auth"/.test(signatureInput);
373
+ const hasSig2 = /sig2=\(/.test(signatureInput);
374
+ const isTAPStandard = hasAgentTag && hasSig2;
375
+ // BOTCHA extended: requires x-tap-agent-id + x-tap-intent + signature
376
+ const hasBOTCHAExtended = Boolean(tapHeaders['x-tap-agent-id'] &&
204
377
  tapHeaders['x-tap-intent'] &&
205
378
  tapHeaders['signature'] &&
206
379
  tapHeaders['signature-input']);
207
- return { hasTAPHeaders, tapHeaders };
380
+ const hasTAPHeaders = isTAPStandard || hasBOTCHAExtended;
381
+ return { hasTAPHeaders, tapHeaders, isTAPStandard };
208
382
  }
209
383
  // ============ VERIFICATION MODES ============
210
384
  /**
@@ -226,6 +400,14 @@ export function getVerificationMode(headers) {
226
400
  }
227
401
  return { mode, hasTAPHeaders, hasChallenge };
228
402
  }
403
+ // ============ TAG MAPPING ============
404
+ /**
405
+ * Map TAP action to appropriate tag
406
+ */
407
+ export function actionToTag(action) {
408
+ const payerActions = ['purchase'];
409
+ return payerActions.includes(action) ? 'agent-payer-auth' : 'agent-browser-auth';
410
+ }
229
411
  // ============ CHALLENGE RESPONSE BUILDERS ============
230
412
  /**
231
413
  * Build appropriate challenge response for TAP verification failure
@@ -262,7 +444,8 @@ export function buildTAPChallengeResponse(verificationResult, challengeData) {
262
444
  'signature',
263
445
  'signature-input'
264
446
  ],
265
- supported_algorithms: ['ecdsa-p256-sha256', 'rsa-pss-sha256']
447
+ supported_algorithms: ['ed25519', 'Ed25519', 'ecdsa-p256-sha256', 'rsa-pss-sha256'],
448
+ jwks_url: 'https://botcha.ai/.well-known/jwks'
266
449
  };
267
450
  return response;
268
451
  }
@@ -271,5 +454,7 @@ export default {
271
454
  parseTAPIntent,
272
455
  extractTAPHeaders,
273
456
  getVerificationMode,
274
- buildTAPChallengeResponse
457
+ buildTAPChallengeResponse,
458
+ checkAndStoreNonce,
459
+ actionToTag
275
460
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dupecom/botcha-cloudflare",
3
- "version": "0.15.0",
3
+ "version": "0.18.0",
4
4
  "description": "BOTCHA for Cloudflare Workers - Prove you're a bot. Humans need not apply.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",