@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
|
@@ -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
|
+
};
|