@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.
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 +13 -4
  12. package/dist/apps.d.ts.map +1 -1
  13. package/dist/apps.js +30 -4
  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 +5 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +660 -83
  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 +781 -5
  42. package/dist/static.d.ts.map +1 -1
  43. package/dist/static.js +790 -111
  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,481 @@
1
+ /**
2
+ * BOTCHA ANS Integration — tap-ans.ts
3
+ *
4
+ * Agent Name Service (ANS) is the emerging "DNS for AI agents" led by GoDaddy.
5
+ * ANS names like `ans://v1.0.myagent.example.com` resolve via DNS TXT records
6
+ * to structured agent metadata. This module makes BOTCHA the verification badge
7
+ * on top of ANS's domain-level trust.
8
+ *
9
+ * ANS gives you DV-level trust (domain exists). BOTCHA adds:
10
+ * "this agent actually behaves like an AI."
11
+ *
12
+ * Together: ANS names the agent, BOTCHA verifies it.
13
+ *
14
+ * Key ANS spec: https://agentnameregistry.org
15
+ * ANS format: ans://v1.0.<label>.<domain>
16
+ * e.g. ans://v1.0.myagent.example.com
17
+ * DNS lookup: TXT record at _ans.<domain>
18
+ * e.g. _ans.example.com TXT "v=ANS1 ..."
19
+ *
20
+ * References:
21
+ * - https://agentnameregistry.org
22
+ * - GoDaddy ANS Marketplace (Dec 2025)
23
+ * - IETF ANS draft (Nov 2025)
24
+ */
25
+ import { SignJWT } from 'jose';
26
+ // ============ ANS NAME PARSING ============
27
+ /**
28
+ * Parse an ANS name into its components.
29
+ *
30
+ * Accepts:
31
+ * - ans://v1.0.myagent.example.com
32
+ * - v1.0.myagent.example.com (without scheme)
33
+ * - myagent.example.com (bare domain, defaults to v1.0)
34
+ * - example.com (root domain, label = domain apex)
35
+ */
36
+ export function parseANSName(input) {
37
+ let raw = input.trim();
38
+ // Strip scheme
39
+ const withoutScheme = raw.startsWith('ans://')
40
+ ? raw.slice('ans://'.length)
41
+ : raw;
42
+ // Split by dots
43
+ const parts = withoutScheme.split('.');
44
+ if (parts.length < 2) {
45
+ return { success: false, error: `Invalid ANS name: "${input}" — must have at least 2 parts` };
46
+ }
47
+ let version = 'v1.0';
48
+ let label;
49
+ let domainParts;
50
+ // Detect version prefix: v1.0, v1, v2.0, etc.
51
+ // Match at the string level (before dot-splitting) to correctly capture "v1.0" as a unit.
52
+ const versionMatch = withoutScheme.match(/^(v\d+(?:\.\d+)?)\./);
53
+ if (versionMatch) {
54
+ version = versionMatch[1]; // "v1.0" or "v1"
55
+ const rest = withoutScheme.slice(version.length + 1); // "myagent.example.com"
56
+ const restParts = rest.split('.');
57
+ if (restParts.length < 2) {
58
+ return { success: false, error: `ANS name with version requires: v<ver>.<label>.<domain>` };
59
+ }
60
+ label = restParts[0];
61
+ domainParts = restParts.slice(1);
62
+ }
63
+ else if (parts.length === 2) {
64
+ // Root domain (e.g. "botcha.ai"): the entire name is the domain identity.
65
+ // label = apex label ("botcha"), domain = full name ("botcha.ai")
66
+ // dnsLookupName = "_ans.botcha.ai" (not "_ans.ai" which is uncontrollable)
67
+ label = parts[0];
68
+ domainParts = parts; // domain = full 2-part name
69
+ }
70
+ else {
71
+ // 3+ parts without version: label is first part, domain is the rest
72
+ // e.g. "myagent.example.com" → label="myagent", domain="example.com"
73
+ label = parts[0];
74
+ domainParts = parts.slice(1);
75
+ }
76
+ const domain = domainParts.join('.');
77
+ // For 2-part root domains, fqdn == domain (no separate subdomain label)
78
+ const fqdn = parts.length === 2 ? domain : `${label}.${domain}`;
79
+ // DNS TXT lookup lives at _ans.<domain>
80
+ const dnsLookupName = `_ans.${domain}`;
81
+ const components = {
82
+ raw: input,
83
+ version,
84
+ label,
85
+ domain,
86
+ fqdn,
87
+ dnsLookupName,
88
+ };
89
+ return { success: true, components };
90
+ }
91
+ // ============ DNS RESOLUTION (Cloudflare DNS-over-HTTPS) ============
92
+ /**
93
+ * Resolve DNS TXT records using Cloudflare's DNS-over-HTTPS API.
94
+ * Works inside Cloudflare Workers (no Node.js dns module needed).
95
+ */
96
+ async function resolveTXTRecords(name) {
97
+ try {
98
+ const url = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(name)}&type=TXT`;
99
+ const response = await fetch(url, {
100
+ headers: {
101
+ 'Accept': 'application/dns-json',
102
+ },
103
+ });
104
+ if (!response.ok) {
105
+ return { records: [], error: `DNS query failed: HTTP ${response.status}` };
106
+ }
107
+ const data = await response.json();
108
+ // Status 0 = NOERROR, Status 3 = NXDOMAIN
109
+ if (data.Status !== 0) {
110
+ return { records: [], error: `DNS status ${data.Status} (NXDOMAIN or error)` };
111
+ }
112
+ if (!data.Answer || data.Answer.length === 0) {
113
+ return { records: [] };
114
+ }
115
+ // Filter TXT records (type 16), strip surrounding quotes
116
+ const txtRecords = data.Answer
117
+ .filter(a => a.type === 16)
118
+ .map(a => a.data.replace(/^"|"$/g, '').replace(/"\s*"/g, '')); // handle multi-string TXT
119
+ return { records: txtRecords };
120
+ }
121
+ catch (err) {
122
+ return { records: [], error: `DNS fetch error: ${err instanceof Error ? err.message : String(err)}` };
123
+ }
124
+ }
125
+ /**
126
+ * Parse a raw ANS TXT record string into structured fields.
127
+ * Format: v=ANS1 name=myagent pub=<base64> cap=browse,search url=https://...
128
+ */
129
+ function parseANSTXTRecord(raw) {
130
+ if (!raw.includes('v=ANS1') && !raw.includes('v=ANS')) {
131
+ return null;
132
+ }
133
+ const record = { version: 'ANS1', raw };
134
+ // Extract key=value pairs (values may be quoted)
135
+ const kvPattern = /(\w+)=("(?:[^"\\]|\\.)*"|[^\s]+)/g;
136
+ let match;
137
+ while ((match = kvPattern.exec(raw)) !== null) {
138
+ const key = match[1];
139
+ const value = match[2].replace(/^"|"$/g, '');
140
+ switch (key) {
141
+ case 'v':
142
+ record.version = value.replace('v=', '').trim() || 'ANS1';
143
+ break;
144
+ case 'name':
145
+ record.name = value;
146
+ break;
147
+ case 'pub':
148
+ record.pub = value;
149
+ break;
150
+ case 'cap':
151
+ record.cap = value.split(',').map(s => s.trim()).filter(Boolean);
152
+ break;
153
+ case 'url':
154
+ record.url = value;
155
+ break;
156
+ case 'did':
157
+ record.did = value;
158
+ break;
159
+ }
160
+ }
161
+ return record;
162
+ }
163
+ // ============ ANS RESOLUTION ============
164
+ /**
165
+ * Resolve an ANS name to agent metadata.
166
+ *
167
+ * Steps:
168
+ * 1. Parse the ANS name
169
+ * 2. Look up DNS TXT at _ans.<domain>
170
+ * 3. Parse ANS TXT record
171
+ * 4. Optionally fetch Agent Card from record.url
172
+ */
173
+ export async function resolveANSName(ansName) {
174
+ const parsed = parseANSName(ansName);
175
+ if (!parsed.success || !parsed.components) {
176
+ return { success: false, error: parsed.error };
177
+ }
178
+ const { components } = parsed;
179
+ // DNS TXT lookup
180
+ const dnsResult = await resolveTXTRecords(components.dnsLookupName);
181
+ if (dnsResult.error && dnsResult.records.length === 0) {
182
+ return {
183
+ success: false,
184
+ name: components,
185
+ error: `DNS lookup failed for ${components.dnsLookupName}: ${dnsResult.error}`,
186
+ };
187
+ }
188
+ // Find ANS record among TXT records
189
+ let ansRecord = null;
190
+ for (const txt of dnsResult.records) {
191
+ const parsed = parseANSTXTRecord(txt);
192
+ if (parsed) {
193
+ ansRecord = parsed;
194
+ break;
195
+ }
196
+ }
197
+ if (!ansRecord) {
198
+ return {
199
+ success: false,
200
+ name: components,
201
+ error: `No ANS TXT record found at ${components.dnsLookupName}. Found ${dnsResult.records.length} TXT record(s) but none matched ANS format (v=ANS1).`,
202
+ };
203
+ }
204
+ // Optionally fetch Agent Card from URL
205
+ let agentCard;
206
+ if (ansRecord.url) {
207
+ try {
208
+ const cardResponse = await fetch(ansRecord.url, {
209
+ headers: { 'Accept': 'application/json' },
210
+ signal: AbortSignal.timeout(5000),
211
+ });
212
+ if (cardResponse.ok) {
213
+ agentCard = await cardResponse.json();
214
+ }
215
+ }
216
+ catch {
217
+ // Non-fatal: agent card fetch failed, continue without it
218
+ }
219
+ }
220
+ return {
221
+ success: true,
222
+ name: components,
223
+ record: ansRecord,
224
+ agentCard,
225
+ resolvedAt: Date.now(),
226
+ };
227
+ }
228
+ /**
229
+ * Verify ANS name ownership via key signature.
230
+ * The ANS TXT record must contain `pub=<base64-pubkey>`.
231
+ */
232
+ export async function verifyANSOwnership(proof, resolvedRecord) {
233
+ // Determine which public key to use
234
+ const pubKeyB64 = proof.public_key || resolvedRecord.pub;
235
+ if (!pubKeyB64) {
236
+ return {
237
+ verified: false,
238
+ method: 'key-ownership',
239
+ error: 'No public key available in ANS record or proof. Add pub=<base64-key> to your _ans TXT record.',
240
+ };
241
+ }
242
+ try {
243
+ // Decode base64url public key (SPKI format expected)
244
+ const pubKeyBytes = base64urlDecode(pubKeyB64);
245
+ // Determine algorithm (default: ECDSA P-256)
246
+ const algorithm = proof.algorithm?.toUpperCase() || 'ECDSA-P256';
247
+ const cryptoAlg = algorithm === 'ED25519'
248
+ ? { name: 'Ed25519' }
249
+ : { name: 'ECDSA', namedCurve: 'P-256' };
250
+ const verifyAlg = algorithm === 'ED25519'
251
+ ? { name: 'Ed25519' }
252
+ : { name: 'ECDSA', hash: { name: 'SHA-256' } };
253
+ // Import public key
254
+ const cryptoKey = await crypto.subtle.importKey('spki', pubKeyBytes, cryptoAlg, false, ['verify']);
255
+ // Decode and verify signature over nonce
256
+ const sigBytes = base64urlDecode(proof.signature);
257
+ const nonceBytes = new TextEncoder().encode(proof.nonce);
258
+ const isValid = await crypto.subtle.verify(verifyAlg, cryptoKey, sigBytes, nonceBytes);
259
+ return {
260
+ verified: isValid,
261
+ method: 'key-ownership',
262
+ error: isValid ? undefined : 'Signature verification failed — nonce not signed by ANS key',
263
+ };
264
+ }
265
+ catch (err) {
266
+ return {
267
+ verified: false,
268
+ method: 'key-ownership',
269
+ error: `Crypto error: ${err instanceof Error ? err.message : String(err)}`,
270
+ };
271
+ }
272
+ }
273
+ // ============ BOTCHA CREDENTIAL ISSUANCE ============
274
+ /**
275
+ * Issue a BOTCHA-ANS linked verification badge.
276
+ *
277
+ * The badge is a signed JWT (HS256) containing:
278
+ * - ans_name: canonical ANS identifier
279
+ * - domain: verified domain
280
+ * - trust_level: what was verified
281
+ * - botcha: "verified" — the stamp
282
+ *
283
+ * This badge can be embedded in ANS Marketplace listings and verified
284
+ * by anyone who knows the BOTCHA JWT secret (or JWKS public key).
285
+ */
286
+ export async function issueANSBadge(components, trustLevel, jwtSecret, options) {
287
+ const now = Date.now();
288
+ const expiresInMs = (options?.expiresInSeconds ?? 86400 * 90) * 1000; // default 90 days
289
+ const expiresAt = now + expiresInMs;
290
+ const badgeId = generateId('ans');
291
+ const payload = {
292
+ type: 'botcha-ans-badge',
293
+ jti: badgeId,
294
+ ans_name: components.raw,
295
+ domain: components.domain,
296
+ label: components.label,
297
+ trust_level: trustLevel,
298
+ botcha: 'verified',
299
+ issuer: 'botcha.ai',
300
+ };
301
+ if (options?.agentId) {
302
+ payload.agent_id = options.agentId;
303
+ }
304
+ if (options?.capabilities) {
305
+ payload.capabilities = options.capabilities;
306
+ }
307
+ const secretBytes = new TextEncoder().encode(jwtSecret);
308
+ const token = await new SignJWT(payload)
309
+ .setProtectedHeader({ alg: 'HS256' })
310
+ .setSubject(components.fqdn)
311
+ .setIssuer('botcha.ai')
312
+ .setIssuedAt()
313
+ .setExpirationTime(Math.floor(expiresAt / 1000))
314
+ .sign(secretBytes);
315
+ return {
316
+ badge_id: badgeId,
317
+ ans_name: components.raw,
318
+ domain: components.domain,
319
+ agent_id: options?.agentId,
320
+ verified: true,
321
+ verification_type: trustLevel === 'domain-validated' ? 'dns-ownership'
322
+ : trustLevel === 'key-validated' ? 'key-ownership'
323
+ : 'challenge-verified',
324
+ trust_level: trustLevel,
325
+ credential_token: token,
326
+ issued_at: now,
327
+ expires_at: expiresAt,
328
+ issuer: 'botcha.ai',
329
+ };
330
+ }
331
+ // ============ ANS DISCOVERY REGISTRY ============
332
+ /**
333
+ * Save a verified ANS agent to the BOTCHA discovery registry.
334
+ * Stored in KV under `ans_registry:<domain>:<label>`
335
+ */
336
+ export async function saveANSRegistryEntry(kv, entry) {
337
+ const key = `ans_registry:${entry.domain}:${entry.label}`;
338
+ const ttlSeconds = Math.max(1, Math.floor((entry.expires_at - Date.now()) / 1000));
339
+ await kv.put(key, JSON.stringify(entry), { expirationTtl: ttlSeconds });
340
+ // Also maintain a global index for listing
341
+ const indexKey = 'ans_registry_index';
342
+ const existingRaw = await kv.get(indexKey);
343
+ const index = existingRaw ? JSON.parse(existingRaw) : [];
344
+ const entryKey = `${entry.domain}:${entry.label}`;
345
+ if (!index.includes(entryKey)) {
346
+ index.push(entryKey);
347
+ await kv.put(indexKey, JSON.stringify(index));
348
+ }
349
+ }
350
+ /**
351
+ * Get a single ANS registry entry.
352
+ */
353
+ export async function getANSRegistryEntry(kv, domain, label) {
354
+ const key = `ans_registry:${domain}:${label}`;
355
+ const raw = await kv.get(key);
356
+ if (!raw)
357
+ return null;
358
+ return JSON.parse(raw);
359
+ }
360
+ /**
361
+ * List all BOTCHA-verified ANS agents in the discovery registry.
362
+ * Optionally filter by domain.
363
+ */
364
+ export async function listANSRegistry(kv, options) {
365
+ const indexKey = 'ans_registry_index';
366
+ const indexRaw = await kv.get(indexKey);
367
+ if (!indexRaw)
368
+ return [];
369
+ const index = JSON.parse(indexRaw);
370
+ const limit = options?.limit ?? 100;
371
+ const domainFilter = options?.domain;
372
+ const filtered = domainFilter
373
+ ? index.filter(k => k.startsWith(`${domainFilter}:`))
374
+ : index;
375
+ const entries = [];
376
+ for (const entryKey of filtered.slice(0, limit)) {
377
+ const [domain, label] = entryKey.split(':', 2);
378
+ const entry = await getANSRegistryEntry(kv, domain, label);
379
+ if (entry) {
380
+ entries.push(entry);
381
+ }
382
+ }
383
+ return entries.sort((a, b) => b.verified_at - a.verified_at);
384
+ }
385
+ // ============ NONCE MANAGEMENT ============
386
+ /**
387
+ * Generate and store a fresh nonce for ANS ownership verification.
388
+ * TTL: 10 minutes — caller must sign and return within this window.
389
+ */
390
+ export async function generateANSNonce(kv, ansName) {
391
+ const nonce = generateId('nonce');
392
+ const key = buildANSNonceKey(ansName);
393
+ await kv.put(key, JSON.stringify({ nonce, created_at: Date.now() }), {
394
+ expirationTtl: 600, // 10 minutes
395
+ });
396
+ return nonce;
397
+ }
398
+ /**
399
+ * Consume (verify + delete) a stored ANS nonce.
400
+ * Returns true if nonce matches. Nonces are single-use.
401
+ */
402
+ export async function consumeANSNonce(kv, ansName, providedNonce) {
403
+ const key = buildANSNonceKey(ansName);
404
+ const raw = await kv.get(key);
405
+ if (!raw)
406
+ return false;
407
+ const stored = JSON.parse(raw);
408
+ if (stored.nonce !== providedNonce)
409
+ return false;
410
+ // Delete nonce — single use
411
+ await kv.delete(key);
412
+ return true;
413
+ }
414
+ function buildANSNonceKey(ansName) {
415
+ const parsed = parseANSName(ansName);
416
+ if (parsed.success && parsed.components) {
417
+ const canonical = `ans://${parsed.components.version}.${parsed.components.fqdn}`.toLowerCase();
418
+ return `ans_nonce:${canonical}`;
419
+ }
420
+ return `ans_nonce:${ansName.trim().toLowerCase()}`;
421
+ }
422
+ // ============ BOTCHA'S OWN ANS RECORD ============
423
+ /**
424
+ * Returns the metadata for BOTCHA's own ANS registration.
425
+ * ans://v1.0.botcha.ai
426
+ *
427
+ * This would be placed as a TXT record at _ans.botcha.ai:
428
+ * v=ANS1 name=botcha cap=verify,issue,discover url=https://botcha.ai/.well-known/agent.json
429
+ */
430
+ export function getBotchaANSRecord() {
431
+ return {
432
+ ans_name: 'ans://v1.0.botcha.ai',
433
+ dns_name: '_ans.botcha.ai',
434
+ txt_record: 'v=ANS1 name=botcha cap=verify,issue,discover url=https://botcha.ai/.well-known/agent.json did=did:web:botcha.ai',
435
+ agent_card: {
436
+ '@context': 'https://schema.org',
437
+ '@type': 'SoftwareApplication',
438
+ name: 'BOTCHA',
439
+ description: 'Reverse CAPTCHA for AI agents. Verification layer for the Agent Name Service.',
440
+ url: 'https://botcha.ai',
441
+ identifier: 'ans://v1.0.botcha.ai',
442
+ did: 'did:web:botcha.ai',
443
+ capabilities: ['verify', 'issue', 'discover'],
444
+ verification: {
445
+ type: 'ANS-BOTCHA-Badge',
446
+ endpoint: 'https://botcha.ai/v1/ans/verify',
447
+ discovery: 'https://botcha.ai/v1/ans/discover',
448
+ },
449
+ },
450
+ };
451
+ }
452
+ // ============ UTILITY ============
453
+ function generateId(prefix) {
454
+ const bytes = new Uint8Array(12);
455
+ crypto.getRandomValues(bytes);
456
+ const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
457
+ return `${prefix}_${hex}`;
458
+ }
459
+ function base64urlDecode(input) {
460
+ // Convert base64url to base64
461
+ const base64 = input.replace(/-/g, '+').replace(/_/g, '/');
462
+ const padded = base64 + '=='.slice(0, (4 - base64.length % 4) % 4);
463
+ const binary = atob(padded);
464
+ const bytes = new Uint8Array(binary.length);
465
+ for (let i = 0; i < binary.length; i++) {
466
+ bytes[i] = binary.charCodeAt(i);
467
+ }
468
+ return bytes;
469
+ }
470
+ export default {
471
+ parseANSName,
472
+ resolveANSName,
473
+ verifyANSOwnership,
474
+ issueANSBadge,
475
+ saveANSRegistryEntry,
476
+ getANSRegistryEntry,
477
+ listANSRegistry,
478
+ generateANSNonce,
479
+ consumeANSNonce,
480
+ getBotchaANSRecord,
481
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"tap-delegation-routes.d.ts","sourceRoot":"","sources":["../src/tap-delegation-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAgEpC;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAsGrD;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAmDlD;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oEAqEpD;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;oEAsErD;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAkDrD;;;;;;;;AAED,wBAME"}
1
+ {"version":3,"file":"tap-delegation-routes.d.ts","sourceRoot":"","sources":["../src/tap-delegation-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAiEpC;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAiHrD;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAmDlD;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oEAqEpD;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;oEAiFrD;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAkDrD;;;;;;;;AAED,wBAME"}
@@ -12,6 +12,7 @@
12
12
  * POST /v1/verify/delegation — Verify delegation chain
13
13
  */
14
14
  import { extractBearerToken, verifyToken, getSigningPublicKeyJWK } from './auth.js';
15
+ import { triggerWebhook } from './webhooks.js';
15
16
  import { TAP_VALID_ACTIONS } from './tap-agents.js';
16
17
  import { createDelegation, getDelegation, listDelegations, revokeDelegation, verifyDelegationChain, } from './tap-delegation.js';
17
18
  // ============ VALIDATION HELPERS ============
@@ -120,6 +121,11 @@ export async function createDelegationRoute(c) {
120
121
  }, status);
121
122
  }
122
123
  const del = result.delegation;
124
+ // Webhook: delegation.created
125
+ const delCreateCtx = c.executionCtx;
126
+ if (delCreateCtx?.waitUntil) {
127
+ delCreateCtx.waitUntil(triggerWebhook(c.env.AGENTS, del.app_id, 'delegation.created', { delegation_id: del.delegation_id, grantor_id: del.grantor_id, grantee_id: del.grantee_id }));
128
+ }
123
129
  return c.json({
124
130
  success: true,
125
131
  delegation_id: del.delegation_id,
@@ -320,6 +326,11 @@ export async function revokeDelegationRoute(c) {
320
326
  }, 500);
321
327
  }
322
328
  const del = result.delegation;
329
+ // Webhook: delegation.revoked
330
+ const delRevokeCtx = c.executionCtx;
331
+ if (delRevokeCtx?.waitUntil) {
332
+ delRevokeCtx.waitUntil(triggerWebhook(c.env.AGENTS, del.app_id, 'delegation.revoked', { delegation_id: del.delegation_id, reason: del.revocation_reason }));
333
+ }
323
334
  return c.json({
324
335
  success: true,
325
336
  delegation_id: del.delegation_id,
@@ -0,0 +1,140 @@
1
+ /**
2
+ * BOTCHA DID — W3C DID Core 1.0 + did:web Method
3
+ *
4
+ * Implements:
5
+ * - BOTCHA DID Document generation (did:web:botcha.ai)
6
+ * - Basic did:web resolver (fetch DID Documents from remote hosts)
7
+ * - DID parsing and validation utilities
8
+ * - Agent DID helpers (did:web:botcha.ai:agents:<agent_id>)
9
+ *
10
+ * Standards:
11
+ * - W3C DID Core 1.0: https://www.w3.org/TR/did-core/
12
+ * - did:web method: https://w3c-ccg.github.io/did-web/
13
+ */
14
+ export interface VerificationMethod {
15
+ id: string;
16
+ type: string;
17
+ controller: string;
18
+ publicKeyJwk?: Record<string, string | undefined>;
19
+ publicKeyMultibase?: string;
20
+ }
21
+ export interface ServiceEndpoint {
22
+ id: string;
23
+ type: string;
24
+ serviceEndpoint: string | string[] | Record<string, string>;
25
+ description?: string;
26
+ }
27
+ export interface DIDDocument {
28
+ '@context': string | string[];
29
+ id: string;
30
+ controller?: string | string[];
31
+ verificationMethod?: VerificationMethod[];
32
+ authentication?: (string | VerificationMethod)[];
33
+ assertionMethod?: (string | VerificationMethod)[];
34
+ keyAgreement?: (string | VerificationMethod)[];
35
+ capabilityInvocation?: (string | VerificationMethod)[];
36
+ capabilityDelegation?: (string | VerificationMethod)[];
37
+ service?: ServiceEndpoint[];
38
+ [key: string]: unknown;
39
+ }
40
+ export interface DIDResolutionResult {
41
+ '@context': string;
42
+ didDocument: DIDDocument | null;
43
+ didResolutionMetadata: {
44
+ contentType?: string;
45
+ error?: string;
46
+ retrieved?: string;
47
+ duration?: number;
48
+ };
49
+ didDocumentMetadata: {
50
+ created?: string;
51
+ updated?: string;
52
+ deactivated?: boolean;
53
+ };
54
+ }
55
+ export interface DIDParseResult {
56
+ valid: boolean;
57
+ method?: string;
58
+ methodSpecificId?: string;
59
+ error?: string;
60
+ }
61
+ /**
62
+ * Generate the canonical BOTCHA DID Document for did:web:botcha.ai.
63
+ *
64
+ * The ES256 signing key from BOTCHA's JWKS is registered as the
65
+ * assertionMethod — meaning VCs signed by this key are cryptographically
66
+ * tied to the botcha.ai DID Document.
67
+ *
68
+ * If no signing key is available (e.g. HS256-only deployment), the
69
+ * verificationMethod array is empty and VCs cannot be verified offline.
70
+ */
71
+ export declare function generateBotchaDIDDocument(baseUrl: string, signingPublicKeyJwk?: {
72
+ kty: string;
73
+ crv?: string;
74
+ x?: string;
75
+ y?: string;
76
+ kid?: string;
77
+ use?: string;
78
+ alg?: string;
79
+ }): DIDDocument;
80
+ /**
81
+ * Parse and validate a DID string.
82
+ *
83
+ * Valid DID format: did:method:method-specific-id
84
+ * - must start with "did:"
85
+ * - method: lowercase alphanumeric
86
+ * - method-specific-id: non-empty
87
+ */
88
+ export declare function parseDID(did: string): DIDParseResult;
89
+ /**
90
+ * Convert a did:web DID to an HTTPS URL for DID Document fetching.
91
+ *
92
+ * Spec rules (https://w3c-ccg.github.io/did-web/):
93
+ * did:web:example.com → https://example.com/.well-known/did.json
94
+ * did:web:example.com:user:alice → https://example.com/user/alice/did.json
95
+ * did:web:example.com%3A8080 → https://example.com:8080/.well-known/did.json
96
+ *
97
+ * Algorithm:
98
+ * 1. Split the method-specific-id on unencoded ':' characters.
99
+ * 2. The first segment is the host — percent-decode it (e.g. %3A → ':' for port).
100
+ * 3. Remaining segments are path components — join with '/'.
101
+ *
102
+ * Returns null if the DID cannot be converted to a valid URL.
103
+ */
104
+ export declare function didWebToUrl(did: string): string | null;
105
+ /**
106
+ * Resolve a did:web DID by fetching its DID Document from the network.
107
+ *
108
+ * Supports:
109
+ * - did:web:example.com (fetches /.well-known/did.json)
110
+ * - did:web:example.com:path:resource (fetches /path/resource/did.json)
111
+ *
112
+ * Note: Only did:web is supported. Other methods return methodNotSupported.
113
+ * Cloudflare Workers support outbound fetch, so this works in production.
114
+ */
115
+ export declare function resolveDIDWeb(did: string): Promise<DIDResolutionResult>;
116
+ /**
117
+ * Build a did:web DID for a BOTCHA-registered agent.
118
+ * agent_abc123 → did:web:botcha.ai:agents:agent_abc123
119
+ */
120
+ export declare function buildAgentDID(agentId: string): string;
121
+ /**
122
+ * Extract agent_id from a BOTCHA agent DID, if applicable.
123
+ * Returns null if the DID is not a BOTCHA agent DID.
124
+ */
125
+ export declare function parseAgentDID(did: string): string | null;
126
+ /**
127
+ * Check if a DID is a valid did:web DID (basic format validation).
128
+ */
129
+ export declare function isValidDIDWeb(did: string): boolean;
130
+ declare const _default: {
131
+ generateBotchaDIDDocument: typeof generateBotchaDIDDocument;
132
+ parseDID: typeof parseDID;
133
+ didWebToUrl: typeof didWebToUrl;
134
+ resolveDIDWeb: typeof resolveDIDWeb;
135
+ buildAgentDID: typeof buildAgentDID;
136
+ parseAgentDID: typeof parseAgentDID;
137
+ isValidDIDWeb: typeof isValidDIDWeb;
138
+ };
139
+ export default _default;
140
+ //# sourceMappingURL=tap-did.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tap-did.d.ts","sourceRoot":"","sources":["../src/tap-did.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAClD,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC/B,kBAAkB,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC1C,cAAc,CAAC,EAAE,CAAC,MAAM,GAAG,kBAAkB,CAAC,EAAE,CAAC;IACjD,eAAe,CAAC,EAAE,CAAC,MAAM,GAAG,kBAAkB,CAAC,EAAE,CAAC;IAClD,YAAY,CAAC,EAAE,CAAC,MAAM,GAAG,kBAAkB,CAAC,EAAE,CAAC;IAC/C,oBAAoB,CAAC,EAAE,CAAC,MAAM,GAAG,kBAAkB,CAAC,EAAE,CAAC;IACvD,oBAAoB,CAAC,EAAE,CAAC,MAAM,GAAG,kBAAkB,CAAC,EAAE,CAAC;IACvD,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;IAE5B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,qBAAqB,EAAE;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,mBAAmB,EAAE;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,WAAW,CAAC,EAAE,OAAO,CAAC;KACvB,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAYD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,mBAAmB,CAAC,EAAE;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GACA,WAAW,CAqDb;AAID;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CA8BpD;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAwBtD;AAID;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAmE7E;AAID;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKxD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAKlD;;;;;;;;;;AAoBD,wBAQE"}