@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,535 @@
1
+ /**
2
+ * BOTCHA ANS API Routes — tap-ans-routes.ts
3
+ *
4
+ * HTTP route handlers for ANS (Agent Name Service) integration.
5
+ *
6
+ * Routes:
7
+ * GET /v1/ans/resolve/:name — Resolve ANS name to agent metadata
8
+ * POST /v1/ans/verify — Issue BOTCHA verification badge for ANS name
9
+ * GET /v1/ans/discover — List BOTCHA-verified ANS agents
10
+ * GET /v1/ans/nonce/:name — Get a nonce for ANS ownership proof
11
+ * GET /v1/ans/botcha — BOTCHA's own ANS record / identity
12
+ *
13
+ * Trust levels issued:
14
+ * domain-validated — ANS TXT record exists and resolves correctly
15
+ * key-validated — Caller proved control of the ANS keypair
16
+ * behavior-validated — Caller also passed a BOTCHA speed/reasoning challenge
17
+ */
18
+ import { extractBearerToken, verifyToken, getSigningPublicKeyJWK } from './auth.js';
19
+ import { parseANSName, resolveANSName, verifyANSOwnership, issueANSBadge, saveANSRegistryEntry, listANSRegistry, generateANSNonce, consumeANSNonce, getBotchaANSRecord, } from './tap-ans.js';
20
+ import { getTAPAgent } from './tap-agents.js';
21
+ // ============ HELPERS ============
22
+ function getVerificationPublicKey(env) {
23
+ const rawSigningKey = env?.JWT_SIGNING_KEY;
24
+ if (!rawSigningKey)
25
+ return undefined;
26
+ try {
27
+ const signingKey = JSON.parse(rawSigningKey);
28
+ return getSigningPublicKeyJWK(signingKey);
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ }
34
+ async function getOptionalAppId(c) {
35
+ const queryAppId = c.req.query('app_id');
36
+ if (queryAppId)
37
+ return queryAppId;
38
+ const authHeader = c.req.header('authorization');
39
+ const token = extractBearerToken(authHeader);
40
+ if (!token)
41
+ return undefined;
42
+ const publicKey = getVerificationPublicKey(c.env);
43
+ const result = await verifyToken(token, c.env.JWT_SECRET, c.env, undefined, publicKey);
44
+ if (result.valid && result.payload?.app_id) {
45
+ return result.payload.app_id;
46
+ }
47
+ return undefined;
48
+ }
49
+ /**
50
+ * Sanitize an ANS name parameter from the URL.
51
+ * Handles both path params and query strings.
52
+ */
53
+ function sanitizeANSName(input) {
54
+ // Decode URI component (handles %3A for colons, etc.)
55
+ try {
56
+ return decodeURIComponent(input).trim();
57
+ }
58
+ catch {
59
+ return input.trim();
60
+ }
61
+ }
62
+ // ============ ROUTE HANDLERS ============
63
+ /**
64
+ * GET /v1/ans/resolve/:name
65
+ *
66
+ * Resolve an ANS name to agent metadata via DNS TXT lookup.
67
+ *
68
+ * Accepts:
69
+ * - Path param: /v1/ans/resolve/ans%3A%2F%2Fv1.0.myagent.example.com
70
+ * - Path param: /v1/ans/resolve/myagent.example.com
71
+ * - Query param: /v1/ans/resolve/lookup?name=ans://v1.0.myagent.example.com
72
+ *
73
+ * Returns:
74
+ * - Parsed ANS name components
75
+ * - DNS TXT record fields (name, pub, cap, url, did)
76
+ * - Optional Agent Card (if record.url is set)
77
+ * - BOTCHA verification status (if already in registry)
78
+ */
79
+ export async function resolveANSNameRoute(c) {
80
+ try {
81
+ // Support both path param and query param
82
+ const pathParam = c.req.param('name');
83
+ const queryParam = c.req.query('name');
84
+ const rawName = sanitizeANSName(pathParam || queryParam || '');
85
+ if (!rawName) {
86
+ return c.json({
87
+ success: false,
88
+ error: 'MISSING_NAME',
89
+ message: 'ANS name is required. Provide as path param or ?name= query.',
90
+ examples: [
91
+ '/v1/ans/resolve/myagent.example.com',
92
+ '/v1/ans/resolve/v1.0.myagent.example.com',
93
+ '/v1/ans/resolve/lookup?name=ans://v1.0.myagent.example.com',
94
+ ],
95
+ }, 400);
96
+ }
97
+ const result = await resolveANSName(rawName);
98
+ if (!result.success) {
99
+ return c.json({
100
+ success: false,
101
+ error: 'RESOLUTION_FAILED',
102
+ message: result.error,
103
+ name: result.name ? {
104
+ raw: result.name.raw,
105
+ domain: result.name.domain,
106
+ label: result.name.label,
107
+ dns_lookup: result.name.dnsLookupName,
108
+ } : undefined,
109
+ hints: [
110
+ `Add a DNS TXT record at ${result.name?.dnsLookupName ?? '_ans.<domain>'}`,
111
+ 'Format: v=ANS1 name=<label> pub=<base64-pubkey> cap=browse,search url=https://...',
112
+ 'Reference: https://agentnameregistry.org',
113
+ ],
114
+ }, 404);
115
+ }
116
+ // Check if this ANS name is already BOTCHA-verified
117
+ const registryEntry = await (async () => {
118
+ try {
119
+ const key = `ans_registry:${result.name.domain}:${result.name.label}`;
120
+ const raw = await c.env.AGENTS.get(key);
121
+ return raw ? JSON.parse(raw) : null;
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ })();
127
+ return c.json({
128
+ success: true,
129
+ ans_name: {
130
+ raw: result.name.raw,
131
+ canonical: `ans://${result.name.version}.${result.name.fqdn}`,
132
+ version: result.name.version,
133
+ label: result.name.label,
134
+ domain: result.name.domain,
135
+ fqdn: result.name.fqdn,
136
+ dns_lookup_name: result.name.dnsLookupName,
137
+ },
138
+ record: result.record ? {
139
+ version: result.record.version,
140
+ name: result.record.name,
141
+ capabilities: result.record.cap || [],
142
+ agent_card_url: result.record.url,
143
+ did: result.record.did,
144
+ has_public_key: Boolean(result.record.pub),
145
+ } : null,
146
+ agent_card: result.agentCard || null,
147
+ botcha_verified: Boolean(registryEntry),
148
+ botcha_badge: registryEntry ? {
149
+ badge_id: registryEntry.badge_id,
150
+ trust_level: registryEntry.trust_level,
151
+ verified_at: new Date(registryEntry.verified_at).toISOString(),
152
+ expires_at: new Date(registryEntry.expires_at).toISOString(),
153
+ } : null,
154
+ resolved_at: result.resolvedAt ? new Date(result.resolvedAt).toISOString() : null,
155
+ get_verified: {
156
+ note: 'Get a BOTCHA verification badge for this ANS name',
157
+ endpoint: 'POST /v1/ans/verify',
158
+ body: {
159
+ ans_name: rawName,
160
+ nonce: '<from GET /v1/ans/nonce/:name>',
161
+ signature: '<sign nonce with your ANS private key>',
162
+ },
163
+ },
164
+ });
165
+ }
166
+ catch (error) {
167
+ console.error('ANS resolve error:', error);
168
+ return c.json({
169
+ success: false,
170
+ error: 'INTERNAL_ERROR',
171
+ message: 'Internal server error during ANS resolution',
172
+ }, 500);
173
+ }
174
+ }
175
+ /**
176
+ * GET /v1/ans/nonce/:name
177
+ *
178
+ * Get a fresh nonce for ANS ownership verification.
179
+ * The caller must sign this nonce with their ANS private key,
180
+ * then submit it to POST /v1/ans/verify.
181
+ *
182
+ * Nonces expire in 10 minutes (single use).
183
+ */
184
+ export async function getANSNonceRoute(c) {
185
+ try {
186
+ const pathParam = c.req.param('name');
187
+ const queryParam = c.req.query('name');
188
+ const rawName = sanitizeANSName(pathParam || queryParam || '');
189
+ if (!rawName) {
190
+ return c.json({
191
+ success: false,
192
+ error: 'MISSING_NAME',
193
+ message: 'ANS name is required',
194
+ }, 400);
195
+ }
196
+ const parsed = parseANSName(rawName);
197
+ if (!parsed.success || !parsed.components) {
198
+ return c.json({
199
+ success: false,
200
+ error: 'INVALID_ANS_NAME',
201
+ message: parsed.error,
202
+ }, 400);
203
+ }
204
+ const nonce = await generateANSNonce(c.env.AGENTS, rawName);
205
+ return c.json({
206
+ success: true,
207
+ nonce,
208
+ ans_name: rawName,
209
+ expires_in_seconds: 600,
210
+ instructions: {
211
+ step1: 'Sign this nonce with the private key corresponding to the pub= key in your _ans TXT record',
212
+ step2: 'Submit to POST /v1/ans/verify with {ans_name, nonce, signature}',
213
+ algorithm: 'ECDSA-P256 (sign nonce bytes, return base64url signature)',
214
+ note: 'Nonce is single-use and expires in 10 minutes',
215
+ },
216
+ });
217
+ }
218
+ catch (error) {
219
+ console.error('ANS nonce error:', error);
220
+ return c.json({
221
+ success: false,
222
+ error: 'INTERNAL_ERROR',
223
+ message: 'Internal server error',
224
+ }, 500);
225
+ }
226
+ }
227
+ /**
228
+ * POST /v1/ans/verify
229
+ *
230
+ * Issue a BOTCHA verification badge for an ANS name.
231
+ *
232
+ * Input (JSON body):
233
+ * ans_name string — ANS name to verify (e.g. "ans://v1.0.myagent.example.com")
234
+ * nonce string — From GET /v1/ans/nonce/:name (required for key-validated+)
235
+ * signature string — base64url signature of nonce (required for key-validated+)
236
+ * algorithm string — "ECDSA-P256" (default) | "Ed25519"
237
+ * agent_id string — optional BOTCHA agent ID to link to this ANS name
238
+ *
239
+ * Trust levels:
240
+ * - domain-validated: ANS TXT record exists (no nonce/signature required)
241
+ * - key-validated: Caller signs nonce with ANS key (nonce + signature required)
242
+ * - behavior-validated: (future) key-validated + BOTCHA challenge passed
243
+ *
244
+ * Returns:
245
+ * BOTCHA-ANS verification badge (JWT credential)
246
+ */
247
+ export async function verifyANSNameRoute(c) {
248
+ try {
249
+ // Auth check FIRST — before any DNS work or body parsing
250
+ const authHeader = c.req.header('authorization');
251
+ const token = extractBearerToken(authHeader);
252
+ if (!token) {
253
+ return c.json({
254
+ success: false,
255
+ error: 'UNAUTHORIZED',
256
+ message: 'Bearer token required. Get a token via POST /v1/challenges/{id}/verify',
257
+ }, 401);
258
+ }
259
+ const publicKey = getVerificationPublicKey(c.env);
260
+ const tokenResult = await verifyToken(token, c.env.JWT_SECRET, c.env, undefined, publicKey);
261
+ if (!tokenResult.valid) {
262
+ return c.json({
263
+ success: false,
264
+ error: 'INVALID_TOKEN',
265
+ message: 'Token is invalid or expired',
266
+ }, 401);
267
+ }
268
+ const tokenAppId = tokenResult.payload?.app_id;
269
+ if (!tokenAppId) {
270
+ return c.json({
271
+ success: false,
272
+ error: 'MISSING_APP_ID',
273
+ message: 'Token is missing app_id claim. Request a token scoped to your app.',
274
+ }, 403);
275
+ }
276
+ const body = await c.req.json().catch(() => ({}));
277
+ const { ans_name, nonce, signature, algorithm, agent_id } = body;
278
+ if (!ans_name) {
279
+ return c.json({
280
+ success: false,
281
+ error: 'MISSING_ANS_NAME',
282
+ message: 'ans_name is required',
283
+ }, 400);
284
+ }
285
+ // Step 1: Resolve the ANS name
286
+ const resolution = await resolveANSName(ans_name);
287
+ if (!resolution.success || !resolution.name || !resolution.record) {
288
+ return c.json({
289
+ success: false,
290
+ error: 'ANS_RESOLUTION_FAILED',
291
+ message: resolution.error || 'Could not resolve ANS name',
292
+ hint: 'Ensure a valid ANS TXT record exists at the domain',
293
+ }, 422);
294
+ }
295
+ const { name: components, record } = resolution;
296
+ // Step 2: Determine trust level based on what caller provides
297
+ let trustLevel = 'domain-validated';
298
+ if (nonce && signature) {
299
+ // Verify nonce was issued by BOTCHA
300
+ const nonceValid = await consumeANSNonce(c.env.AGENTS, ans_name, nonce);
301
+ if (!nonceValid) {
302
+ return c.json({
303
+ success: false,
304
+ error: 'INVALID_NONCE',
305
+ message: 'Nonce is invalid, expired, or already used. Get a fresh nonce from GET /v1/ans/nonce/:name',
306
+ }, 400);
307
+ }
308
+ // Verify ownership
309
+ const ownershipResult = await verifyANSOwnership({ ans_name, nonce, signature, algorithm }, record);
310
+ if (!ownershipResult.verified) {
311
+ return c.json({
312
+ success: false,
313
+ error: 'OWNERSHIP_VERIFICATION_FAILED',
314
+ message: ownershipResult.error,
315
+ hint: 'Sign the exact nonce bytes with the private key corresponding to the pub= key in your _ans TXT record',
316
+ }, 403);
317
+ }
318
+ trustLevel = 'key-validated';
319
+ }
320
+ // Step 3: Validate linked agent_id if provided
321
+ let resolvedAgentId = agent_id;
322
+ if (agent_id) {
323
+ const agentResult = await getTAPAgent(c.env.AGENTS, agent_id);
324
+ if (!agentResult.success) {
325
+ return c.json({
326
+ success: false,
327
+ error: 'AGENT_NOT_FOUND',
328
+ message: `Agent ${agent_id} not found in BOTCHA registry`,
329
+ }, 404);
330
+ }
331
+ if (agentResult.agent?.app_id !== tokenAppId) {
332
+ return c.json({
333
+ success: false,
334
+ error: 'APP_ID_MISMATCH',
335
+ message: 'Agent belongs to a different app. You can only verify ANS names for agents in your own app.',
336
+ }, 403);
337
+ }
338
+ }
339
+ // Step 4: Issue the badge
340
+ const badge = await issueANSBadge(components, trustLevel, c.env.JWT_SECRET, {
341
+ agentId: resolvedAgentId,
342
+ capabilities: record.cap,
343
+ agentCardUrl: record.url,
344
+ });
345
+ // Step 5: Save to discovery registry
346
+ const registryEntry = {
347
+ ans_name: components.raw,
348
+ domain: components.domain,
349
+ label: components.label,
350
+ agent_id: resolvedAgentId,
351
+ badge_id: badge.badge_id,
352
+ trust_level: trustLevel,
353
+ capabilities: record.cap,
354
+ agent_card_url: record.url,
355
+ verified_at: badge.issued_at,
356
+ expires_at: badge.expires_at,
357
+ };
358
+ await saveANSRegistryEntry(c.env.AGENTS, registryEntry);
359
+ // Step 6: Update agent record with ans_name if agent_id provided
360
+ if (resolvedAgentId) {
361
+ try {
362
+ const agentRaw = await c.env.AGENTS.get(`agent:${resolvedAgentId}`);
363
+ if (agentRaw) {
364
+ const agent = JSON.parse(agentRaw);
365
+ agent.ans_name = components.raw;
366
+ agent.ans_badge_id = badge.badge_id;
367
+ agent.ans_trust_level = trustLevel;
368
+ agent.ans_verified_at = badge.issued_at;
369
+ await c.env.AGENTS.put(`agent:${resolvedAgentId}`, JSON.stringify(agent));
370
+ }
371
+ }
372
+ catch (err) {
373
+ // Non-fatal: continue if agent update fails
374
+ console.error('Failed to update agent with ANS info:', err);
375
+ }
376
+ }
377
+ return c.json({
378
+ success: true,
379
+ badge: {
380
+ badge_id: badge.badge_id,
381
+ ans_name: badge.ans_name,
382
+ domain: badge.domain,
383
+ agent_id: badge.agent_id,
384
+ verified: badge.verified,
385
+ trust_level: badge.trust_level,
386
+ verification_type: badge.verification_type,
387
+ credential_token: badge.credential_token,
388
+ issued_at: new Date(badge.issued_at).toISOString(),
389
+ expires_at: new Date(badge.expires_at).toISOString(),
390
+ issuer: badge.issuer,
391
+ },
392
+ record: {
393
+ capabilities: record.cap || [],
394
+ agent_card_url: record.url,
395
+ did: record.did,
396
+ has_public_key: Boolean(record.pub),
397
+ },
398
+ trust_levels: {
399
+ current: trustLevel,
400
+ description: trustLevel === 'domain-validated'
401
+ ? 'ANS TXT record exists and resolves. Domain ownership not cryptographically proven.'
402
+ : trustLevel === 'key-validated'
403
+ ? 'Caller proved control of the ANS keypair. Strong ownership proof.'
404
+ : 'Full behavior verification: keypair + BOTCHA challenge passed.',
405
+ upgrade: trustLevel === 'domain-validated' ? {
406
+ to: 'key-validated',
407
+ how: 'Get a nonce from GET /v1/ans/nonce/:name and sign it with your ANS private key',
408
+ } : null,
409
+ },
410
+ discovery: {
411
+ note: 'This agent is now listed in the BOTCHA ANS discovery registry',
412
+ endpoint: 'GET /v1/ans/discover',
413
+ },
414
+ }, 201);
415
+ }
416
+ catch (error) {
417
+ console.error('ANS verify error:', error);
418
+ return c.json({
419
+ success: false,
420
+ error: 'INTERNAL_ERROR',
421
+ message: 'Internal server error during ANS verification',
422
+ }, 500);
423
+ }
424
+ }
425
+ /**
426
+ * GET /v1/ans/discover
427
+ *
428
+ * List BOTCHA-verified ANS agents in the public discovery registry.
429
+ * These agents have passed BOTCHA verification and are safe to interact with.
430
+ *
431
+ * Query params:
432
+ * domain string — filter by domain (e.g. ?domain=example.com)
433
+ * limit number — max results (default: 50, max: 100)
434
+ */
435
+ export async function discoverANSAgentsRoute(c) {
436
+ try {
437
+ const domain = c.req.query('domain');
438
+ const limitParam = parseInt(c.req.query('limit') || '50', 10);
439
+ const limit = Math.min(Math.max(1, isNaN(limitParam) ? 50 : limitParam), 100);
440
+ const entries = await listANSRegistry(c.env.AGENTS, { domain, limit });
441
+ const agents = entries.map(e => ({
442
+ ans_name: e.ans_name,
443
+ domain: e.domain,
444
+ label: e.label,
445
+ agent_id: e.agent_id,
446
+ trust_level: e.trust_level,
447
+ capabilities: e.capabilities || [],
448
+ agent_card_url: e.agent_card_url,
449
+ verified_at: new Date(e.verified_at).toISOString(),
450
+ expires_at: new Date(e.expires_at).toISOString(),
451
+ badge_id: e.badge_id,
452
+ }));
453
+ return c.json({
454
+ success: true,
455
+ count: agents.length,
456
+ agents,
457
+ registry: {
458
+ description: 'BOTCHA-verified ANS agents. These agents have proven domain ownership and passed BOTCHA verification.',
459
+ trust_levels: {
460
+ 'domain-validated': 'ANS TXT record exists',
461
+ 'key-validated': 'Proven control of ANS keypair',
462
+ 'behavior-validated': 'Keypair + BOTCHA challenge verified',
463
+ },
464
+ get_verified: 'POST /v1/ans/verify',
465
+ resolve_name: 'GET /v1/ans/resolve/:name',
466
+ },
467
+ filter: domain ? { domain } : null,
468
+ });
469
+ }
470
+ catch (error) {
471
+ console.error('ANS discover error:', error);
472
+ return c.json({
473
+ success: false,
474
+ error: 'INTERNAL_ERROR',
475
+ message: 'Internal server error during ANS discovery',
476
+ }, 500);
477
+ }
478
+ }
479
+ /**
480
+ * GET /v1/ans/botcha
481
+ *
482
+ * BOTCHA's own ANS record and identity.
483
+ * ans://v1.0.botcha.ai
484
+ *
485
+ * This endpoint serves as BOTCHA's Agent Card for ANS-aware clients.
486
+ * Also returns the DNS TXT record that should be published at _ans.botcha.ai.
487
+ */
488
+ export async function getBotchaANSRoute(c) {
489
+ try {
490
+ const botchaRecord = getBotchaANSRecord();
491
+ return c.json({
492
+ success: true,
493
+ identity: {
494
+ ans_name: botchaRecord.ans_name,
495
+ canonical: 'ans://v1.0.botcha.ai',
496
+ dns_record: {
497
+ name: botchaRecord.dns_name,
498
+ type: 'TXT',
499
+ value: botchaRecord.txt_record,
500
+ },
501
+ },
502
+ agent_card: botchaRecord.agent_card,
503
+ endpoints: {
504
+ resolve: 'GET /v1/ans/resolve/:name',
505
+ verify: 'POST /v1/ans/verify',
506
+ discover: 'GET /v1/ans/discover',
507
+ nonce: 'GET /v1/ans/nonce/:name',
508
+ },
509
+ integration: {
510
+ note: 'BOTCHA is the verification layer for ANS. ANS names the agent, BOTCHA verifies it.',
511
+ ans_spec: 'https://agentnameregistry.org',
512
+ botcha_docs: 'https://botcha.ai/ai.txt',
513
+ trust_model: {
514
+ 'ANS alone': 'DV-level trust — domain exists',
515
+ 'ANS + BOTCHA': 'Full stack — domain exists AND agent behaves like an AI',
516
+ },
517
+ },
518
+ });
519
+ }
520
+ catch (error) {
521
+ console.error('ANS botcha identity error:', error);
522
+ return c.json({
523
+ success: false,
524
+ error: 'INTERNAL_ERROR',
525
+ message: 'Internal server error',
526
+ }, 500);
527
+ }
528
+ }
529
+ export default {
530
+ resolveANSNameRoute,
531
+ getANSNonceRoute,
532
+ verifyANSNameRoute,
533
+ discoverANSAgentsRoute,
534
+ getBotchaANSRoute,
535
+ };