@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-a2a.js
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Agent Card Attestation — BOTCHA as A2A Trust Oracle
|
|
3
|
+
*
|
|
4
|
+
* Implements Google's A2A (Agent-to-Agent) protocol trust layer:
|
|
5
|
+
* 1. BOTCHA's own Agent Card (/.well-known/agent.json)
|
|
6
|
+
* 2. Agent Card attestation issuance (POST /v1/a2a/attest)
|
|
7
|
+
* 3. Agent Card attestation verification (POST /v1/a2a/verify-card)
|
|
8
|
+
* 4. Verified card registry (GET /v1/a2a/cards)
|
|
9
|
+
*
|
|
10
|
+
* Attestation model:
|
|
11
|
+
* - Input: any A2A Agent Card JSON
|
|
12
|
+
* - Output: a signed JWT embedding SHA-256(canonicalized card)
|
|
13
|
+
* - The JWT is embedded into card.extensions.botcha_attestation
|
|
14
|
+
* - Verification checks: signature, hash match, expiration, revocation
|
|
15
|
+
*
|
|
16
|
+
* References:
|
|
17
|
+
* https://google.github.io/A2A/
|
|
18
|
+
* https://github.com/google-a2a/A2A
|
|
19
|
+
*/
|
|
20
|
+
import { SignJWT, jwtVerify } from 'jose';
|
|
21
|
+
// ============ BOTCHA'S OWN AGENT CARD ============
|
|
22
|
+
export const BOTCHA_VERSION = '0.21.2';
|
|
23
|
+
export const BOTCHA_URL = 'https://botcha.ai';
|
|
24
|
+
/**
|
|
25
|
+
* BOTCHA's A2A Agent Card.
|
|
26
|
+
* Served at /.well-known/agent.json
|
|
27
|
+
*/
|
|
28
|
+
export function getBotchaAgentCard(version) {
|
|
29
|
+
const v = version || BOTCHA_VERSION;
|
|
30
|
+
return {
|
|
31
|
+
name: 'BOTCHA',
|
|
32
|
+
description: 'Reverse CAPTCHA for AI agents. Prove you\'re a bot. Humans need not apply.',
|
|
33
|
+
url: BOTCHA_URL,
|
|
34
|
+
version: v,
|
|
35
|
+
documentationUrl: 'https://botcha.ai/docs',
|
|
36
|
+
capabilities: {
|
|
37
|
+
streaming: false,
|
|
38
|
+
pushNotifications: false,
|
|
39
|
+
stateTransitionHistory: false,
|
|
40
|
+
},
|
|
41
|
+
authentication: [
|
|
42
|
+
{
|
|
43
|
+
schemes: ['Bearer'],
|
|
44
|
+
description: 'BOTCHA access token — obtain via POST /v1/token/verify after solving a challenge',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
defaultInputModes: ['application/json'],
|
|
48
|
+
defaultOutputModes: ['application/json'],
|
|
49
|
+
skills: [
|
|
50
|
+
{
|
|
51
|
+
id: 'verify-agent',
|
|
52
|
+
name: 'Verify Agent',
|
|
53
|
+
description: 'Issue a BOTCHA challenge to verify an AI agent. Returns a signed access token on success.',
|
|
54
|
+
tags: ['verification', 'identity', 'challenge'],
|
|
55
|
+
examples: [
|
|
56
|
+
'GET /v1/token?app_id=<app_id> → solve SHA256 challenge → POST /v1/token/verify',
|
|
57
|
+
],
|
|
58
|
+
inputModes: ['application/json'],
|
|
59
|
+
outputModes: ['application/json'],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'attest-card',
|
|
63
|
+
name: 'Attest Agent Card',
|
|
64
|
+
description: 'Issue a BOTCHA attestation for an A2A Agent Card. The attestation JWT is embedded into the card\'s extensions.botcha_attestation field, making BOTCHA the trust oracle for the A2A ecosystem.',
|
|
65
|
+
tags: ['attestation', 'a2a', 'trust', 'verification'],
|
|
66
|
+
examples: [
|
|
67
|
+
'POST /v1/a2a/attest with an A2A Agent Card JSON → attested card with extensions.botcha_attestation',
|
|
68
|
+
],
|
|
69
|
+
inputModes: ['application/json'],
|
|
70
|
+
outputModes: ['application/json'],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'verify-card',
|
|
74
|
+
name: 'Verify Attested Agent Card',
|
|
75
|
+
description: 'Verify a BOTCHA-attested A2A Agent Card. Checks signature, card hash integrity, and expiration.',
|
|
76
|
+
tags: ['verification', 'a2a', 'attestation', 'trust'],
|
|
77
|
+
examples: [
|
|
78
|
+
'POST /v1/a2a/verify-card with an attested A2A Agent Card JSON',
|
|
79
|
+
],
|
|
80
|
+
inputModes: ['application/json'],
|
|
81
|
+
outputModes: ['application/json'],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'check-reputation',
|
|
85
|
+
name: 'Check Reputation',
|
|
86
|
+
description: 'Get an agent\'s BOTCHA reputation score based on challenge history and verification track record.',
|
|
87
|
+
tags: ['reputation', 'trust', 'score'],
|
|
88
|
+
examples: [
|
|
89
|
+
'GET /v1/reputation/:agent_id',
|
|
90
|
+
],
|
|
91
|
+
inputModes: ['application/json'],
|
|
92
|
+
outputModes: ['application/json'],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
extensions: {
|
|
96
|
+
botcha: {
|
|
97
|
+
challenge_endpoint: `${BOTCHA_URL}/v1/token`,
|
|
98
|
+
verify_endpoint: `${BOTCHA_URL}/v1/token/verify`,
|
|
99
|
+
attest_endpoint: `${BOTCHA_URL}/v1/a2a/attest`,
|
|
100
|
+
verify_card_endpoint: `${BOTCHA_URL}/v1/a2a/verify-card`,
|
|
101
|
+
registry_endpoint: `${BOTCHA_URL}/v1/a2a/cards`,
|
|
102
|
+
openapi: `${BOTCHA_URL}/openapi.json`,
|
|
103
|
+
ai_txt: `${BOTCHA_URL}/ai.txt`,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// ============ CARD CANONICALIZATION & HASHING ============
|
|
109
|
+
/**
|
|
110
|
+
* Canonicalize an A2A Agent Card for hashing.
|
|
111
|
+
*
|
|
112
|
+
* We remove the extensions.botcha_attestation field before hashing
|
|
113
|
+
* so that the attestation can be embedded in the card without
|
|
114
|
+
* invalidating its own hash. All other fields are included.
|
|
115
|
+
*
|
|
116
|
+
* Canonicalization: JSON.stringify with sorted keys (deterministic).
|
|
117
|
+
*/
|
|
118
|
+
export function canonicalizeCard(card) {
|
|
119
|
+
// Deep clone and strip the attestation extension
|
|
120
|
+
const stripped = deepCloneWithoutAttestation(card);
|
|
121
|
+
// Sort keys recursively for deterministic output
|
|
122
|
+
return stableStringify(stripped);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Compute SHA-256 of a canonicalized card string.
|
|
126
|
+
* Returns lowercase hex string.
|
|
127
|
+
*/
|
|
128
|
+
export async function hashCard(card) {
|
|
129
|
+
const canonical = canonicalizeCard(card);
|
|
130
|
+
const encoder = new TextEncoder();
|
|
131
|
+
const data = encoder.encode(canonical);
|
|
132
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
133
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
134
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
135
|
+
}
|
|
136
|
+
function deepCloneWithoutAttestation(card) {
|
|
137
|
+
const clone = {};
|
|
138
|
+
for (const [key, value] of Object.entries(card)) {
|
|
139
|
+
if (key === 'extensions') {
|
|
140
|
+
if (value && typeof value === 'object') {
|
|
141
|
+
const exts = { ...value };
|
|
142
|
+
delete exts['botcha_attestation'];
|
|
143
|
+
if (Object.keys(exts).length > 0) {
|
|
144
|
+
clone['extensions'] = deepCloneValue(exts);
|
|
145
|
+
}
|
|
146
|
+
// If extensions only had botcha_attestation, omit extensions entirely
|
|
147
|
+
}
|
|
148
|
+
// else: no extensions, skip
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
clone[key] = deepCloneValue(value);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return clone;
|
|
155
|
+
}
|
|
156
|
+
function deepCloneValue(value) {
|
|
157
|
+
if (value === null || typeof value !== 'object')
|
|
158
|
+
return value;
|
|
159
|
+
if (Array.isArray(value))
|
|
160
|
+
return value.map(deepCloneValue);
|
|
161
|
+
const obj = value;
|
|
162
|
+
const result = {};
|
|
163
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
164
|
+
result[k] = deepCloneValue(v);
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Stable (deterministic) JSON stringify with sorted keys.
|
|
170
|
+
* Handles nested objects, arrays, primitives.
|
|
171
|
+
*/
|
|
172
|
+
function stableStringify(value) {
|
|
173
|
+
if (value === null || value === undefined)
|
|
174
|
+
return String(value);
|
|
175
|
+
if (typeof value !== 'object')
|
|
176
|
+
return JSON.stringify(value);
|
|
177
|
+
if (Array.isArray(value)) {
|
|
178
|
+
return '[' + value.map(stableStringify).join(',') + ']';
|
|
179
|
+
}
|
|
180
|
+
const obj = value;
|
|
181
|
+
const keys = Object.keys(obj).sort();
|
|
182
|
+
const pairs = keys
|
|
183
|
+
.filter(k => obj[k] !== undefined)
|
|
184
|
+
.map(k => JSON.stringify(k) + ':' + stableStringify(obj[k]));
|
|
185
|
+
return '{' + pairs.join(',') + '}';
|
|
186
|
+
}
|
|
187
|
+
// ============ ATTESTATION ISSUANCE ============
|
|
188
|
+
const DEFAULT_DURATION = 86400; // 24 hours
|
|
189
|
+
const MAX_DURATION = 2592000; // 30 days
|
|
190
|
+
/**
|
|
191
|
+
* Issue a BOTCHA attestation for an A2A Agent Card.
|
|
192
|
+
*
|
|
193
|
+
* Steps:
|
|
194
|
+
* 1. Validate card (name + url required)
|
|
195
|
+
* 2. Canonicalize and hash the card (without extensions)
|
|
196
|
+
* 3. Sign a JWT with card_hash + agent identity
|
|
197
|
+
* 4. Embed attestation in card.extensions.botcha_attestation
|
|
198
|
+
* 5. Store attestation in KV for lookup and revocation
|
|
199
|
+
*/
|
|
200
|
+
export async function attestCard(sessions, secret, options) {
|
|
201
|
+
try {
|
|
202
|
+
const { card, app_id } = options;
|
|
203
|
+
// Validate required card fields
|
|
204
|
+
if (!card.name || typeof card.name !== 'string') {
|
|
205
|
+
return { success: false, error: 'Agent Card must have a "name" field' };
|
|
206
|
+
}
|
|
207
|
+
if (!card.url || typeof card.url !== 'string') {
|
|
208
|
+
return { success: false, error: 'Agent Card must have a "url" field' };
|
|
209
|
+
}
|
|
210
|
+
// Validate URL format
|
|
211
|
+
try {
|
|
212
|
+
new URL(card.url);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return { success: false, error: `Invalid "url" field: ${card.url}` };
|
|
216
|
+
}
|
|
217
|
+
const trustLevel = options.trust_level || 'verified';
|
|
218
|
+
const durationSeconds = Math.min(options.duration_seconds ?? DEFAULT_DURATION, MAX_DURATION);
|
|
219
|
+
const now = Date.now();
|
|
220
|
+
const expiresAt = now + durationSeconds * 1000;
|
|
221
|
+
const attestationId = crypto.randomUUID();
|
|
222
|
+
// Hash the card (without any existing attestation)
|
|
223
|
+
const cardHash = await hashCard(card);
|
|
224
|
+
// Sign attestation JWT
|
|
225
|
+
const encoder = new TextEncoder();
|
|
226
|
+
const secretKey = encoder.encode(secret);
|
|
227
|
+
const token = await new SignJWT({
|
|
228
|
+
type: 'botcha-a2a-attestation',
|
|
229
|
+
card_hash: cardHash,
|
|
230
|
+
agent_name: card.name,
|
|
231
|
+
agent_url: card.url,
|
|
232
|
+
app_id,
|
|
233
|
+
trust_level: trustLevel,
|
|
234
|
+
jti: attestationId,
|
|
235
|
+
})
|
|
236
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
237
|
+
.setSubject(card.url)
|
|
238
|
+
.setIssuer(BOTCHA_URL)
|
|
239
|
+
.setIssuedAt()
|
|
240
|
+
.setExpirationTime(Math.floor(expiresAt / 1000))
|
|
241
|
+
.sign(secretKey);
|
|
242
|
+
// Build attestation record
|
|
243
|
+
const attestation = {
|
|
244
|
+
attestation_id: attestationId,
|
|
245
|
+
agent_url: card.url,
|
|
246
|
+
agent_name: card.name,
|
|
247
|
+
app_id,
|
|
248
|
+
card_hash: cardHash,
|
|
249
|
+
token,
|
|
250
|
+
trust_level: trustLevel,
|
|
251
|
+
created_at: now,
|
|
252
|
+
expires_at: expiresAt,
|
|
253
|
+
revoked: false,
|
|
254
|
+
card_snapshot: deepCloneWithoutAttestation(card),
|
|
255
|
+
};
|
|
256
|
+
// Store in KV with TTL
|
|
257
|
+
const ttlSeconds = Math.max(60, Math.floor(durationSeconds));
|
|
258
|
+
await sessions.put(`a2a_attestation:${attestationId}`, JSON.stringify(attestation), { expirationTtl: ttlSeconds });
|
|
259
|
+
// Update registry index (agent_url → list of attestation IDs)
|
|
260
|
+
await updateCardRegistryIndex(sessions, card.url, attestationId, 'add');
|
|
261
|
+
// Build the attested card (card + extensions.botcha_attestation)
|
|
262
|
+
const extension = {
|
|
263
|
+
token,
|
|
264
|
+
verified_at: new Date(now).toISOString(),
|
|
265
|
+
trust_level: trustLevel,
|
|
266
|
+
issuer: BOTCHA_URL,
|
|
267
|
+
card_hash: cardHash,
|
|
268
|
+
expires_at: new Date(expiresAt).toISOString(),
|
|
269
|
+
};
|
|
270
|
+
const attestedCard = {
|
|
271
|
+
...card,
|
|
272
|
+
extensions: {
|
|
273
|
+
...(card.extensions || {}),
|
|
274
|
+
botcha_attestation: extension,
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
return { success: true, attestation, attested_card: attestedCard };
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
console.error('A2A card attestation error:', error);
|
|
281
|
+
return { success: false, error: 'Internal server error' };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// ============ ATTESTATION VERIFICATION ============
|
|
285
|
+
/**
|
|
286
|
+
* Verify a BOTCHA-attested A2A Agent Card.
|
|
287
|
+
*
|
|
288
|
+
* Checks:
|
|
289
|
+
* 1. Card has extensions.botcha_attestation.token
|
|
290
|
+
* 2. JWT signature and expiration (cryptographic)
|
|
291
|
+
* 3. JWT type is 'botcha-a2a-attestation'
|
|
292
|
+
* 4. card_hash matches the current card content (tamper detection)
|
|
293
|
+
* 5. Revocation status (KV lookup, fail-open)
|
|
294
|
+
*
|
|
295
|
+
* Returns verified/invalid + decoded claims.
|
|
296
|
+
*/
|
|
297
|
+
export async function verifyCard(sessions, secret, card) {
|
|
298
|
+
try {
|
|
299
|
+
// Extract attestation token from card
|
|
300
|
+
const attestationExt = card.extensions?.botcha_attestation;
|
|
301
|
+
if (!attestationExt || typeof attestationExt !== 'object') {
|
|
302
|
+
return {
|
|
303
|
+
success: true,
|
|
304
|
+
valid: false,
|
|
305
|
+
error: 'NO_ATTESTATION',
|
|
306
|
+
reason: 'Card does not have extensions.botcha_attestation',
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const token = attestationExt.token;
|
|
310
|
+
if (!token || typeof token !== 'string') {
|
|
311
|
+
return {
|
|
312
|
+
success: true,
|
|
313
|
+
valid: false,
|
|
314
|
+
error: 'MISSING_TOKEN',
|
|
315
|
+
reason: 'extensions.botcha_attestation.token is missing',
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
// Verify JWT signature and expiration
|
|
319
|
+
const encoder = new TextEncoder();
|
|
320
|
+
const secretKey = encoder.encode(secret);
|
|
321
|
+
let payload;
|
|
322
|
+
try {
|
|
323
|
+
const result = await jwtVerify(token, secretKey, { algorithms: ['HS256'] });
|
|
324
|
+
payload = result.payload;
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
return {
|
|
328
|
+
success: true,
|
|
329
|
+
valid: false,
|
|
330
|
+
error: 'INVALID_TOKEN',
|
|
331
|
+
reason: err instanceof Error ? err.message : 'JWT verification failed',
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
// Check token type
|
|
335
|
+
if (payload['type'] !== 'botcha-a2a-attestation') {
|
|
336
|
+
return {
|
|
337
|
+
success: true,
|
|
338
|
+
valid: false,
|
|
339
|
+
error: 'WRONG_TOKEN_TYPE',
|
|
340
|
+
reason: `Expected 'botcha-a2a-attestation', got '${payload['type']}'`,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const jti = payload['jti'];
|
|
344
|
+
// Check revocation
|
|
345
|
+
if (jti) {
|
|
346
|
+
try {
|
|
347
|
+
const revoked = await sessions.get(`a2a_attestation_revoked:${jti}`);
|
|
348
|
+
if (revoked) {
|
|
349
|
+
return {
|
|
350
|
+
success: true,
|
|
351
|
+
valid: false,
|
|
352
|
+
error: 'REVOKED',
|
|
353
|
+
reason: 'Attestation has been revoked',
|
|
354
|
+
attestation_id: jti,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// fail-open on KV errors
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Verify card hash matches (tamper detection)
|
|
363
|
+
const tokenCardHash = payload['card_hash'];
|
|
364
|
+
const currentCardHash = await hashCard(card);
|
|
365
|
+
if (tokenCardHash !== currentCardHash) {
|
|
366
|
+
return {
|
|
367
|
+
success: true,
|
|
368
|
+
valid: false,
|
|
369
|
+
error: 'HASH_MISMATCH',
|
|
370
|
+
reason: 'Card content has been modified since attestation. The card hash does not match.',
|
|
371
|
+
card_hash: currentCardHash,
|
|
372
|
+
attestation_id: jti,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
success: true,
|
|
377
|
+
valid: true,
|
|
378
|
+
attestation_id: jti,
|
|
379
|
+
agent_url: payload['agent_url'],
|
|
380
|
+
agent_name: payload['agent_name'],
|
|
381
|
+
card_hash: tokenCardHash,
|
|
382
|
+
trust_level: payload['trust_level'],
|
|
383
|
+
app_id: payload['app_id'],
|
|
384
|
+
issued_at: new Date(payload['iat'] * 1000).toISOString(),
|
|
385
|
+
expires_at: new Date(payload['exp'] * 1000).toISOString(),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
console.error('A2A card verification error:', error);
|
|
390
|
+
return { success: false, error: 'Internal server error' };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// ============ CARD REGISTRY ============
|
|
394
|
+
/**
|
|
395
|
+
* Get a specific A2A attestation record by ID.
|
|
396
|
+
*/
|
|
397
|
+
export async function getCardAttestation(sessions, attestationId) {
|
|
398
|
+
try {
|
|
399
|
+
const data = await sessions.get(`a2a_attestation:${attestationId}`, 'text');
|
|
400
|
+
if (!data) {
|
|
401
|
+
return { success: false, error: 'Attestation not found or expired' };
|
|
402
|
+
}
|
|
403
|
+
return { success: true, attestation: JSON.parse(data) };
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
console.error('Failed to get A2A attestation:', error);
|
|
407
|
+
return { success: false, error: 'Internal server error' };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* List BOTCHA-verified A2A cards from the registry.
|
|
412
|
+
*
|
|
413
|
+
* Query options:
|
|
414
|
+
* verified_only: boolean — only return non-revoked attestations (default: true)
|
|
415
|
+
* agent_url: string — filter by agent URL
|
|
416
|
+
* limit: number — max results (default 50, max 200)
|
|
417
|
+
*/
|
|
418
|
+
export async function listVerifiedCards(sessions, opts = {}) {
|
|
419
|
+
try {
|
|
420
|
+
const verifiedOnly = opts.verified_only !== false;
|
|
421
|
+
const limit = Math.min(opts.limit || 50, 200);
|
|
422
|
+
// If filtering by agent_url, use the per-agent index
|
|
423
|
+
if (opts.agent_url) {
|
|
424
|
+
const indexData = await sessions.get(`a2a_registry:${encodeURIComponent(opts.agent_url)}`, 'text');
|
|
425
|
+
const ids = indexData ? JSON.parse(indexData) : [];
|
|
426
|
+
const results = [];
|
|
427
|
+
for (const id of ids.slice(-limit)) {
|
|
428
|
+
const result = await getCardAttestation(sessions, id);
|
|
429
|
+
if (result.success && result.attestation) {
|
|
430
|
+
if (!verifiedOnly || !result.attestation.revoked) {
|
|
431
|
+
results.push(result.attestation);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return { success: true, attestations: results };
|
|
436
|
+
}
|
|
437
|
+
// Global registry index
|
|
438
|
+
const globalIndexData = await sessions.get('a2a_registry:global', 'text');
|
|
439
|
+
const globalIds = globalIndexData ? JSON.parse(globalIndexData) : [];
|
|
440
|
+
const results = [];
|
|
441
|
+
// Take the most recent `limit` entries
|
|
442
|
+
for (const id of globalIds.slice(-limit).reverse()) {
|
|
443
|
+
if (results.length >= limit)
|
|
444
|
+
break;
|
|
445
|
+
const result = await getCardAttestation(sessions, id);
|
|
446
|
+
if (result.success && result.attestation) {
|
|
447
|
+
if (!verifiedOnly || !result.attestation.revoked) {
|
|
448
|
+
results.push(result.attestation);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return { success: true, attestations: results };
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
console.error('Failed to list A2A attestations:', error);
|
|
456
|
+
return { success: false, error: 'Internal server error' };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// ============ UTILITY ============
|
|
460
|
+
async function updateCardRegistryIndex(sessions, agentUrl, attestationId, operation) {
|
|
461
|
+
try {
|
|
462
|
+
// Per-agent index
|
|
463
|
+
const agentKey = `a2a_registry:${encodeURIComponent(agentUrl)}`;
|
|
464
|
+
const agentData = await sessions.get(agentKey, 'text');
|
|
465
|
+
let agentIds = agentData ? JSON.parse(agentData) : [];
|
|
466
|
+
if (operation === 'add' && !agentIds.includes(attestationId)) {
|
|
467
|
+
agentIds.push(attestationId);
|
|
468
|
+
}
|
|
469
|
+
else if (operation === 'remove') {
|
|
470
|
+
agentIds = agentIds.filter(id => id !== attestationId);
|
|
471
|
+
}
|
|
472
|
+
await sessions.put(agentKey, JSON.stringify(agentIds));
|
|
473
|
+
// Global index
|
|
474
|
+
const globalKey = 'a2a_registry:global';
|
|
475
|
+
const globalData = await sessions.get(globalKey, 'text');
|
|
476
|
+
let globalIds = globalData ? JSON.parse(globalData) : [];
|
|
477
|
+
if (operation === 'add' && !globalIds.includes(attestationId)) {
|
|
478
|
+
globalIds.push(attestationId);
|
|
479
|
+
// Keep global index bounded (last 1000)
|
|
480
|
+
if (globalIds.length > 1000) {
|
|
481
|
+
globalIds = globalIds.slice(-1000);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
else if (operation === 'remove') {
|
|
485
|
+
globalIds = globalIds.filter(id => id !== attestationId);
|
|
486
|
+
}
|
|
487
|
+
await sessions.put(globalKey, JSON.stringify(globalIds));
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
console.error('Failed to update A2A card registry index:', error);
|
|
491
|
+
// Fail silently — index updates are best-effort
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
export default {
|
|
495
|
+
getBotchaAgentCard,
|
|
496
|
+
canonicalizeCard,
|
|
497
|
+
hashCard,
|
|
498
|
+
attestCard,
|
|
499
|
+
verifyCard,
|
|
500
|
+
getCardAttestation,
|
|
501
|
+
listVerifiedCards,
|
|
502
|
+
};
|
package/dist/tap-agents.d.ts
CHANGED
|
@@ -22,6 +22,13 @@ export interface TAPAgent extends Agent {
|
|
|
22
22
|
issuer?: string;
|
|
23
23
|
tap_enabled?: boolean;
|
|
24
24
|
last_verified_at?: number;
|
|
25
|
+
ans_name?: string;
|
|
26
|
+
ans_badge_id?: string;
|
|
27
|
+
ans_trust_level?: 'domain-validated' | 'key-validated' | 'behavior-validated';
|
|
28
|
+
ans_verified_at?: number;
|
|
29
|
+
did?: string;
|
|
30
|
+
provider?: 'anthropic' | 'openai' | 'google' | 'mistral' | 'cohere' | 'other';
|
|
31
|
+
provider_key_hash?: string;
|
|
25
32
|
}
|
|
26
33
|
/** Valid TAP capability actions — single source of truth */
|
|
27
34
|
export declare const TAP_VALID_ACTIONS: readonly ["browse", "compare", "purchase", "audit", "search"];
|
|
@@ -63,6 +70,9 @@ export declare function registerTAPAgent(agents: KVNamespace, appId: string, reg
|
|
|
63
70
|
capabilities?: TAPCapability[];
|
|
64
71
|
trust_level?: 'basic' | 'verified' | 'enterprise';
|
|
65
72
|
issuer?: string;
|
|
73
|
+
ans_name?: string;
|
|
74
|
+
/** Optional W3C DID for this agent — embedded as credentialSubject.id in issued VCs */
|
|
75
|
+
did?: string;
|
|
66
76
|
}): Promise<{
|
|
67
77
|
success: boolean;
|
|
68
78
|
agent?: TAPAgent;
|
|
@@ -104,6 +114,11 @@ export declare function getTAPSession(sessions: KVNamespace, sessionId: string):
|
|
|
104
114
|
session?: TAPSession;
|
|
105
115
|
error?: string;
|
|
106
116
|
}>;
|
|
117
|
+
/**
|
|
118
|
+
* Returns true if the string is a valid JWK JSON (EC, RSA, or OKP public key).
|
|
119
|
+
* Accepts a raw JWK JSON string.
|
|
120
|
+
*/
|
|
121
|
+
export declare function isValidJWK(key: string): boolean;
|
|
107
122
|
export declare function validateCapability(agentCapabilities: TAPCapability[], requiredAction: string, requiredScope?: string): {
|
|
108
123
|
valid: boolean;
|
|
109
124
|
error?: string;
|
package/dist/tap-agents.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tap-agents.d.ts","sourceRoot":"","sources":["../src/tap-agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,KAAK,EAAE,WAAW,EAAmB,MAAM,aAAa,CAAC;AAIlE;;GAEG;AACH,MAAM,WAAW,QAAS,SAAQ,KAAK;IAErC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,mBAAmB,GAAG,gBAAgB,GAAG,SAAS,CAAC;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IAGxB,YAAY,CAAC,EAAE,aAAa,EAAE,CAAC;IAC/B,WAAW,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,YAAY,CAAC;IAGlD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"tap-agents.d.ts","sourceRoot":"","sources":["../src/tap-agents.ts"],"names":[],"mappings":"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"}
|
package/dist/tap-agents.js
CHANGED
|
@@ -34,7 +34,11 @@ export async function registerTAPAgent(agents, appId, registration) {
|
|
|
34
34
|
trust_level: registration.trust_level || 'basic',
|
|
35
35
|
issuer: registration.issuer,
|
|
36
36
|
tap_enabled: Boolean(registration.public_key),
|
|
37
|
-
last_verified_at: undefined
|
|
37
|
+
last_verified_at: undefined,
|
|
38
|
+
// ANS fields
|
|
39
|
+
ans_name: registration.ans_name,
|
|
40
|
+
// W3C DID (optional)
|
|
41
|
+
did: registration.did,
|
|
38
42
|
};
|
|
39
43
|
// Validate TAP configuration
|
|
40
44
|
if (agent.public_key) {
|
|
@@ -175,11 +179,37 @@ function generateSessionId() {
|
|
|
175
179
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
176
180
|
.join('');
|
|
177
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Returns true if the string is a valid JWK JSON (EC, RSA, or OKP public key).
|
|
184
|
+
* Accepts a raw JWK JSON string.
|
|
185
|
+
*/
|
|
186
|
+
export function isValidJWK(key) {
|
|
187
|
+
try {
|
|
188
|
+
const jwk = JSON.parse(key);
|
|
189
|
+
if (!jwk || typeof jwk !== 'object')
|
|
190
|
+
return false;
|
|
191
|
+
if (!jwk.kty)
|
|
192
|
+
return false;
|
|
193
|
+
if (jwk.kty === 'EC')
|
|
194
|
+
return Boolean(jwk.crv && jwk.x && jwk.y);
|
|
195
|
+
if (jwk.kty === 'RSA')
|
|
196
|
+
return Boolean(jwk.n && jwk.e);
|
|
197
|
+
if (jwk.kty === 'OKP')
|
|
198
|
+
return Boolean(jwk.crv && jwk.x);
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
178
205
|
function isValidPublicKey(key, algorithm) {
|
|
179
206
|
// PEM format
|
|
180
207
|
if (key.includes('BEGIN PUBLIC KEY') && key.includes('END PUBLIC KEY') && key.length > 100) {
|
|
181
208
|
return true;
|
|
182
209
|
}
|
|
210
|
+
// JWK JSON string (EC P-256, RSA, or OKP/Ed25519)
|
|
211
|
+
if (isValidJWK(key))
|
|
212
|
+
return true;
|
|
183
213
|
// Raw Ed25519 key (32 bytes = ~44 base64 chars)
|
|
184
214
|
if (algorithm === 'ed25519') {
|
|
185
215
|
const stripped = key.replace(/[\s]/g, '');
|