@dupecom/botcha-cloudflare 0.20.2 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -9
- package/dist/agent-auth.d.ts +129 -0
- package/dist/agent-auth.d.ts.map +1 -0
- package/dist/agent-auth.js +210 -0
- package/dist/agents.d.ts +10 -0
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +51 -1
- package/dist/app-gate.d.ts +6 -0
- package/dist/app-gate.d.ts.map +1 -0
- package/dist/app-gate.js +69 -0
- package/dist/apps.d.ts +13 -4
- package/dist/apps.d.ts.map +1 -1
- package/dist/apps.js +30 -4
- package/dist/dashboard/account.d.ts +63 -0
- package/dist/dashboard/account.d.ts.map +1 -0
- package/dist/dashboard/account.js +488 -0
- package/dist/dashboard/api.js +15 -68
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +14 -14
- package/dist/dashboard/docs.d.ts.map +1 -1
- package/dist/dashboard/docs.js +146 -3
- package/dist/dashboard/layout.d.ts.map +1 -1
- package/dist/dashboard/layout.js +2 -2
- package/dist/dashboard/mcp-setup.d.ts +15 -0
- package/dist/dashboard/mcp-setup.d.ts.map +1 -0
- package/dist/dashboard/mcp-setup.js +391 -0
- package/dist/dashboard/showcase.d.ts +6 -10
- package/dist/dashboard/showcase.d.ts.map +1 -1
- package/dist/dashboard/showcase.js +67 -991
- package/dist/dashboard/whitepaper.d.ts.map +1 -1
- package/dist/dashboard/whitepaper.js +42 -4
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +660 -83
- package/dist/mcp.d.ts +20 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +1290 -0
- package/dist/oauth-agent.d.ts +130 -0
- package/dist/oauth-agent.d.ts.map +1 -0
- package/dist/oauth-agent.js +194 -0
- package/dist/static.d.ts +781 -5
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +790 -111
- package/dist/tap-a2a-routes.d.ts +355 -0
- package/dist/tap-a2a-routes.d.ts.map +1 -0
- package/dist/tap-a2a-routes.js +475 -0
- package/dist/tap-a2a.d.ts +199 -0
- package/dist/tap-a2a.d.ts.map +1 -0
- package/dist/tap-a2a.js +502 -0
- package/dist/tap-agents.d.ts +15 -0
- package/dist/tap-agents.d.ts.map +1 -1
- package/dist/tap-agents.js +31 -1
- package/dist/tap-ans-routes.d.ts +302 -0
- package/dist/tap-ans-routes.d.ts.map +1 -0
- package/dist/tap-ans-routes.js +535 -0
- package/dist/tap-ans.d.ts +241 -0
- package/dist/tap-ans.d.ts.map +1 -0
- package/dist/tap-ans.js +481 -0
- package/dist/tap-delegation-routes.d.ts.map +1 -1
- package/dist/tap-delegation-routes.js +11 -0
- package/dist/tap-did.d.ts +140 -0
- package/dist/tap-did.d.ts.map +1 -0
- package/dist/tap-did.js +262 -0
- package/dist/tap-oidca-routes.d.ts +383 -0
- package/dist/tap-oidca-routes.d.ts.map +1 -0
- package/dist/tap-oidca-routes.js +597 -0
- package/dist/tap-oidca.d.ts +288 -0
- package/dist/tap-oidca.d.ts.map +1 -0
- package/dist/tap-oidca.js +461 -0
- package/dist/tap-routes.d.ts +24 -8
- package/dist/tap-routes.d.ts.map +1 -1
- package/dist/tap-routes.js +169 -23
- package/dist/tap-vc-routes.d.ts +358 -0
- package/dist/tap-vc-routes.d.ts.map +1 -0
- package/dist/tap-vc-routes.js +367 -0
- package/dist/tap-vc.d.ts +125 -0
- package/dist/tap-vc.d.ts.map +1 -0
- package/dist/tap-vc.js +245 -0
- package/dist/tap-x402-routes.d.ts +89 -0
- package/dist/tap-x402-routes.d.ts.map +1 -0
- package/dist/tap-x402-routes.js +579 -0
- package/dist/tap-x402.d.ts +222 -0
- package/dist/tap-x402.d.ts.map +1 -0
- package/dist/tap-x402.js +546 -0
- package/dist/webhooks.d.ts +99 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +642 -0
- package/package.json +3 -1
package/dist/tap-ans.js
ADDED
|
@@ -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;
|
|
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":"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"}
|