@dupecom/botcha-cloudflare 0.16.0 → 0.19.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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/auth.d.ts +48 -3
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +89 -21
  5. package/dist/dashboard/docs.d.ts +15 -0
  6. package/dist/dashboard/docs.d.ts.map +1 -0
  7. package/dist/dashboard/docs.js +556 -0
  8. package/dist/dashboard/layout.d.ts +12 -0
  9. package/dist/dashboard/layout.d.ts.map +1 -1
  10. package/dist/dashboard/layout.js +12 -5
  11. package/dist/dashboard/showcase.d.ts.map +1 -1
  12. package/dist/dashboard/showcase.js +2 -1
  13. package/dist/dashboard/whitepaper.d.ts.map +1 -1
  14. package/dist/dashboard/whitepaper.js +3 -3
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +125 -13
  18. package/dist/static.d.ts +592 -2
  19. package/dist/static.d.ts.map +1 -1
  20. package/dist/static.js +422 -9
  21. package/dist/tap-attestation-routes.d.ts +204 -0
  22. package/dist/tap-attestation-routes.d.ts.map +1 -0
  23. package/dist/tap-attestation-routes.js +396 -0
  24. package/dist/tap-attestation.d.ts +178 -0
  25. package/dist/tap-attestation.d.ts.map +1 -0
  26. package/dist/tap-attestation.js +416 -0
  27. package/dist/tap-delegation-routes.d.ts +236 -0
  28. package/dist/tap-delegation-routes.d.ts.map +1 -0
  29. package/dist/tap-delegation-routes.js +378 -0
  30. package/dist/tap-delegation.d.ts +127 -0
  31. package/dist/tap-delegation.d.ts.map +1 -0
  32. package/dist/tap-delegation.js +490 -0
  33. package/dist/tap-jwks.d.ts +2 -1
  34. package/dist/tap-jwks.d.ts.map +1 -1
  35. package/dist/tap-jwks.js +31 -7
  36. package/dist/tap-reputation-routes.d.ts +154 -0
  37. package/dist/tap-reputation-routes.d.ts.map +1 -0
  38. package/dist/tap-reputation-routes.js +341 -0
  39. package/dist/tap-reputation.d.ts +136 -0
  40. package/dist/tap-reputation.d.ts.map +1 -0
  41. package/dist/tap-reputation.js +346 -0
  42. package/package.json +1 -1
@@ -0,0 +1,178 @@
1
+ /**
2
+ * TAP Capability Attestation
3
+ *
4
+ * Signed JWT tokens that cryptographically bind:
5
+ * WHO (agent_id) can do WHAT (can/cannot rules) on WHICH resources,
6
+ * attested by WHOM (app authority), until WHEN (expiration).
7
+ *
8
+ * Permission model: "action:resource" patterns with explicit deny.
9
+ * - Allow: { can: ["read:invoices", "write:orders", "browse:*"] }
10
+ * - Deny: { cannot: ["write:transfers", "purchase:*"] }
11
+ * - Deny takes precedence over allow
12
+ * - Wildcards: "*:*" (all), "read:*" (read anything), "*:invoices" (any action on invoices)
13
+ * - Backward compatible: bare actions like "browse" expand to "browse:*"
14
+ *
15
+ * Attestation tokens are signed JWTs (HS256) with type 'botcha-attestation'.
16
+ * They can be verified offline (signature check) or online (revocation check via KV).
17
+ */
18
+ import type { KVNamespace } from './agents.js';
19
+ export interface AttestationPayload {
20
+ sub: string;
21
+ iss: string;
22
+ type: 'botcha-attestation';
23
+ jti: string;
24
+ iat: number;
25
+ exp: number;
26
+ can: string[];
27
+ cannot: string[];
28
+ restrictions?: {
29
+ max_amount?: number;
30
+ rate_limit?: number;
31
+ [key: string]: any;
32
+ };
33
+ delegation_id?: string;
34
+ metadata?: Record<string, string>;
35
+ }
36
+ export interface Attestation {
37
+ attestation_id: string;
38
+ agent_id: string;
39
+ app_id: string;
40
+ can: string[];
41
+ cannot: string[];
42
+ restrictions?: {
43
+ max_amount?: number;
44
+ rate_limit?: number;
45
+ [key: string]: any;
46
+ };
47
+ delegation_id?: string;
48
+ metadata?: Record<string, string>;
49
+ token: string;
50
+ created_at: number;
51
+ expires_at: number;
52
+ revoked: boolean;
53
+ revoked_at?: number;
54
+ revocation_reason?: string;
55
+ }
56
+ export interface IssueAttestationOptions {
57
+ agent_id: string;
58
+ can: string[];
59
+ cannot?: string[];
60
+ restrictions?: {
61
+ max_amount?: number;
62
+ rate_limit?: number;
63
+ [key: string]: any;
64
+ };
65
+ duration_seconds?: number;
66
+ delegation_id?: string;
67
+ metadata?: Record<string, string>;
68
+ }
69
+ export interface AttestationResult {
70
+ success: boolean;
71
+ attestation?: Attestation;
72
+ token?: string;
73
+ error?: string;
74
+ }
75
+ export interface CapabilityCheckResult {
76
+ allowed: boolean;
77
+ reason?: string;
78
+ matched_rule?: string;
79
+ }
80
+ /**
81
+ * Normalize a capability string to "action:resource" format.
82
+ * Bare actions like "browse" expand to "browse:*".
83
+ */
84
+ export declare function normalizeCapability(cap: string): string;
85
+ /**
86
+ * Check if a pattern matches a target.
87
+ * Supports wildcards: "*:*", "read:*", "*:invoices", "read:invoices"
88
+ */
89
+ export declare function matchesPattern(pattern: string, target: string): boolean;
90
+ /**
91
+ * Check if a specific action:resource is allowed by the can/cannot rules.
92
+ *
93
+ * Rules:
94
+ * 1. Check "cannot" list first — any match means DENIED (deny takes precedence)
95
+ * 2. Check "can" list — any match means ALLOWED
96
+ * 3. If no match in either list — DENIED (default deny)
97
+ */
98
+ export declare function checkCapability(can: string[], cannot: string[], action: string, resource?: string): CapabilityCheckResult;
99
+ /**
100
+ * Validate capability pattern syntax.
101
+ * Valid: "action:resource", "action", "*:*", "read:*", "*:invoices"
102
+ */
103
+ export declare function isValidCapabilityPattern(pattern: string): boolean;
104
+ /**
105
+ * Issue a capability attestation token for an agent.
106
+ *
107
+ * Validates:
108
+ * - Agent exists and belongs to the app
109
+ * - All capability patterns are syntactically valid
110
+ * - Total rules don't exceed limit
111
+ *
112
+ * Signs a JWT with the attestation payload.
113
+ */
114
+ export declare function issueAttestation(agents: KVNamespace, sessions: KVNamespace, appId: string, secret: string, options: IssueAttestationOptions): Promise<AttestationResult>;
115
+ /**
116
+ * Get an attestation by ID (from KV, not from JWT)
117
+ */
118
+ export declare function getAttestation(sessions: KVNamespace, attestationId: string): Promise<AttestationResult>;
119
+ /**
120
+ * Revoke an attestation.
121
+ */
122
+ export declare function revokeAttestation(sessions: KVNamespace, attestationId: string, reason?: string): Promise<AttestationResult>;
123
+ /**
124
+ * Verify an attestation JWT token.
125
+ *
126
+ * Checks:
127
+ * 1. JWT signature and expiration (cryptographic)
128
+ * 2. Token type is 'botcha-attestation'
129
+ * 3. Revocation status (via KV, fail-open)
130
+ *
131
+ * Returns the parsed attestation payload if valid.
132
+ */
133
+ export declare function verifyAttestationToken(sessions: KVNamespace, token: string, secret: string): Promise<{
134
+ valid: boolean;
135
+ payload?: AttestationPayload;
136
+ error?: string;
137
+ }>;
138
+ /**
139
+ * Full capability check: verify attestation token + check specific action:resource.
140
+ *
141
+ * Combines token verification with permission checking in one call.
142
+ */
143
+ export declare function verifyAndCheckCapability(sessions: KVNamespace, token: string, secret: string, action: string, resource?: string): Promise<{
144
+ allowed: boolean;
145
+ agent_id?: string;
146
+ reason?: string;
147
+ matched_rule?: string;
148
+ error?: string;
149
+ }>;
150
+ /**
151
+ * Create a Hono middleware that enforces capability attestation.
152
+ *
153
+ * Usage:
154
+ * app.get('/api/invoices', requireCapability('read:invoices'), handler);
155
+ * app.post('/api/transfers', requireCapability('write:transfers'), handler);
156
+ *
157
+ * Extracts attestation token from:
158
+ * 1. X-Botcha-Attestation header
159
+ * 2. Authorization: Bearer header (if token type is attestation)
160
+ *
161
+ * On failure: returns 403 with capability denial details.
162
+ * On missing token: returns 401 requesting attestation.
163
+ */
164
+ export declare function requireCapability(capability: string): (c: any, next: () => Promise<void>) => Promise<any>;
165
+ declare const _default: {
166
+ issueAttestation: typeof issueAttestation;
167
+ getAttestation: typeof getAttestation;
168
+ revokeAttestation: typeof revokeAttestation;
169
+ verifyAttestationToken: typeof verifyAttestationToken;
170
+ verifyAndCheckCapability: typeof verifyAndCheckCapability;
171
+ checkCapability: typeof checkCapability;
172
+ matchesPattern: typeof matchesPattern;
173
+ normalizeCapability: typeof normalizeCapability;
174
+ isValidCapabilityPattern: typeof isValidCapabilityPattern;
175
+ requireCapability: typeof requireCapability;
176
+ };
177
+ export default _default;
178
+ //# sourceMappingURL=tap-attestation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tap-attestation.d.ts","sourceRoot":"","sources":["../src/tap-attestation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,416 @@
1
+ /**
2
+ * TAP Capability Attestation
3
+ *
4
+ * Signed JWT tokens that cryptographically bind:
5
+ * WHO (agent_id) can do WHAT (can/cannot rules) on WHICH resources,
6
+ * attested by WHOM (app authority), until WHEN (expiration).
7
+ *
8
+ * Permission model: "action:resource" patterns with explicit deny.
9
+ * - Allow: { can: ["read:invoices", "write:orders", "browse:*"] }
10
+ * - Deny: { cannot: ["write:transfers", "purchase:*"] }
11
+ * - Deny takes precedence over allow
12
+ * - Wildcards: "*:*" (all), "read:*" (read anything), "*:invoices" (any action on invoices)
13
+ * - Backward compatible: bare actions like "browse" expand to "browse:*"
14
+ *
15
+ * Attestation tokens are signed JWTs (HS256) with type 'botcha-attestation'.
16
+ * They can be verified offline (signature check) or online (revocation check via KV).
17
+ */
18
+ import { SignJWT, jwtVerify } from 'jose';
19
+ import { getTAPAgent } from './tap-agents.js';
20
+ // ============ CONSTANTS ============
21
+ const DEFAULT_DURATION = 3600; // 1 hour
22
+ const MAX_DURATION = 86400 * 30; // 30 days
23
+ const MAX_RULES = 100; // max can + cannot entries
24
+ // ============ PERMISSION MATCHING ============
25
+ /**
26
+ * Normalize a capability string to "action:resource" format.
27
+ * Bare actions like "browse" expand to "browse:*".
28
+ */
29
+ export function normalizeCapability(cap) {
30
+ if (cap.includes(':'))
31
+ return cap;
32
+ return `${cap}:*`;
33
+ }
34
+ /**
35
+ * Check if a pattern matches a target.
36
+ * Supports wildcards: "*:*", "read:*", "*:invoices", "read:invoices"
37
+ */
38
+ export function matchesPattern(pattern, target) {
39
+ const normalizedPattern = normalizeCapability(pattern);
40
+ const normalizedTarget = normalizeCapability(target);
41
+ const [patAction, patResource] = normalizedPattern.split(':', 2);
42
+ const [tgtAction, tgtResource] = normalizedTarget.split(':', 2);
43
+ const actionMatch = patAction === '*' || patAction === tgtAction;
44
+ const resourceMatch = patResource === '*' || patResource === tgtResource;
45
+ return actionMatch && resourceMatch;
46
+ }
47
+ /**
48
+ * Check if a specific action:resource is allowed by the can/cannot rules.
49
+ *
50
+ * Rules:
51
+ * 1. Check "cannot" list first — any match means DENIED (deny takes precedence)
52
+ * 2. Check "can" list — any match means ALLOWED
53
+ * 3. If no match in either list — DENIED (default deny)
54
+ */
55
+ export function checkCapability(can, cannot, action, resource) {
56
+ const target = resource ? `${action}:${resource}` : normalizeCapability(action);
57
+ // Check deny rules first (deny takes precedence)
58
+ for (const rule of cannot) {
59
+ if (matchesPattern(rule, target)) {
60
+ return {
61
+ allowed: false,
62
+ reason: `Explicitly denied by rule: ${rule}`,
63
+ matched_rule: rule,
64
+ };
65
+ }
66
+ }
67
+ // Check allow rules
68
+ for (const rule of can) {
69
+ if (matchesPattern(rule, target)) {
70
+ return {
71
+ allowed: true,
72
+ reason: `Allowed by rule: ${rule}`,
73
+ matched_rule: rule,
74
+ };
75
+ }
76
+ }
77
+ // Default deny
78
+ return {
79
+ allowed: false,
80
+ reason: `No matching allow rule for: ${target}`,
81
+ };
82
+ }
83
+ /**
84
+ * Validate capability pattern syntax.
85
+ * Valid: "action:resource", "action", "*:*", "read:*", "*:invoices"
86
+ */
87
+ export function isValidCapabilityPattern(pattern) {
88
+ const normalized = normalizeCapability(pattern);
89
+ const parts = normalized.split(':');
90
+ if (parts.length !== 2)
91
+ return false;
92
+ const [action, resource] = parts;
93
+ // Action and resource must be non-empty, alphanumeric + underscore + hyphen + wildcard
94
+ const validPart = /^[a-zA-Z0-9_\-*]+$/;
95
+ return validPart.test(action) && validPart.test(resource);
96
+ }
97
+ // ============ ATTESTATION ISSUANCE ============
98
+ /**
99
+ * Issue a capability attestation token for an agent.
100
+ *
101
+ * Validates:
102
+ * - Agent exists and belongs to the app
103
+ * - All capability patterns are syntactically valid
104
+ * - Total rules don't exceed limit
105
+ *
106
+ * Signs a JWT with the attestation payload.
107
+ */
108
+ export async function issueAttestation(agents, sessions, appId, secret, options) {
109
+ try {
110
+ // Validate inputs
111
+ if (!options.agent_id) {
112
+ return { success: false, error: 'agent_id is required' };
113
+ }
114
+ if (!options.can || options.can.length === 0) {
115
+ return { success: false, error: 'At least one "can" rule is required' };
116
+ }
117
+ const cannot = options.cannot || [];
118
+ const totalRules = options.can.length + cannot.length;
119
+ if (totalRules > MAX_RULES) {
120
+ return { success: false, error: `Too many rules (${totalRules}). Maximum: ${MAX_RULES}` };
121
+ }
122
+ // Validate all patterns
123
+ for (const rule of options.can) {
124
+ if (!isValidCapabilityPattern(rule)) {
125
+ return { success: false, error: `Invalid capability pattern in "can": ${rule}` };
126
+ }
127
+ }
128
+ for (const rule of cannot) {
129
+ if (!isValidCapabilityPattern(rule)) {
130
+ return { success: false, error: `Invalid capability pattern in "cannot": ${rule}` };
131
+ }
132
+ }
133
+ // Verify agent exists and belongs to this app
134
+ const agentResult = await getTAPAgent(agents, options.agent_id);
135
+ if (!agentResult.success || !agentResult.agent) {
136
+ return { success: false, error: 'Agent not found' };
137
+ }
138
+ if (agentResult.agent.app_id !== appId) {
139
+ return { success: false, error: 'Agent does not belong to this app' };
140
+ }
141
+ // Calculate expiration
142
+ const durationSeconds = Math.min(options.duration_seconds ?? DEFAULT_DURATION, MAX_DURATION);
143
+ const now = Date.now();
144
+ const expiresAt = now + durationSeconds * 1000;
145
+ // Generate attestation ID
146
+ const attestationId = crypto.randomUUID();
147
+ // Sign the attestation JWT
148
+ const encoder = new TextEncoder();
149
+ const secretKey = encoder.encode(secret);
150
+ const payload = {
151
+ type: 'botcha-attestation',
152
+ can: options.can,
153
+ cannot,
154
+ jti: attestationId,
155
+ };
156
+ if (options.restrictions) {
157
+ payload.restrictions = options.restrictions;
158
+ }
159
+ if (options.delegation_id) {
160
+ payload.delegation_id = options.delegation_id;
161
+ }
162
+ if (options.metadata) {
163
+ payload.metadata = options.metadata;
164
+ }
165
+ const token = await new SignJWT(payload)
166
+ .setProtectedHeader({ alg: 'HS256' })
167
+ .setSubject(options.agent_id)
168
+ .setIssuer(appId)
169
+ .setIssuedAt()
170
+ .setExpirationTime(Math.floor(expiresAt / 1000))
171
+ .sign(secretKey);
172
+ // Build attestation record
173
+ const attestation = {
174
+ attestation_id: attestationId,
175
+ agent_id: options.agent_id,
176
+ app_id: appId,
177
+ can: options.can,
178
+ cannot,
179
+ restrictions: options.restrictions,
180
+ delegation_id: options.delegation_id,
181
+ metadata: options.metadata,
182
+ token,
183
+ created_at: now,
184
+ expires_at: expiresAt,
185
+ revoked: false,
186
+ };
187
+ // Store attestation in KV (for revocation and lookup)
188
+ const ttlSeconds = Math.max(1, Math.floor(durationSeconds));
189
+ await sessions.put(`attestation:${attestationId}`, JSON.stringify(attestation), { expirationTtl: ttlSeconds });
190
+ // Update agent's attestation index
191
+ await updateAttestationIndex(sessions, options.agent_id, attestationId, 'add');
192
+ return { success: true, attestation, token };
193
+ }
194
+ catch (error) {
195
+ console.error('Failed to issue attestation:', error);
196
+ return { success: false, error: 'Internal server error' };
197
+ }
198
+ }
199
+ /**
200
+ * Get an attestation by ID (from KV, not from JWT)
201
+ */
202
+ export async function getAttestation(sessions, attestationId) {
203
+ try {
204
+ const data = await sessions.get(`attestation:${attestationId}`, 'text');
205
+ if (!data) {
206
+ return { success: false, error: 'Attestation not found or expired' };
207
+ }
208
+ const attestation = JSON.parse(data);
209
+ return { success: true, attestation };
210
+ }
211
+ catch (error) {
212
+ console.error('Failed to get attestation:', error);
213
+ return { success: false, error: 'Internal server error' };
214
+ }
215
+ }
216
+ /**
217
+ * Revoke an attestation.
218
+ */
219
+ export async function revokeAttestation(sessions, attestationId, reason) {
220
+ try {
221
+ const result = await getAttestation(sessions, attestationId);
222
+ if (!result.success || !result.attestation) {
223
+ return { success: false, error: 'Attestation not found' };
224
+ }
225
+ const attestation = result.attestation;
226
+ if (attestation.revoked) {
227
+ return { success: true, attestation }; // idempotent
228
+ }
229
+ attestation.revoked = true;
230
+ attestation.revoked_at = Date.now();
231
+ attestation.revocation_reason = reason;
232
+ // Re-store with remaining TTL
233
+ const remainingTtl = Math.max(60, Math.floor((attestation.expires_at - Date.now()) / 1000));
234
+ await sessions.put(`attestation:${attestationId}`, JSON.stringify(attestation), { expirationTtl: remainingTtl });
235
+ // Also store in revocation list (for fast JWT verification without full record lookup)
236
+ await sessions.put(`attestation_revoked:${attestationId}`, JSON.stringify({ revokedAt: attestation.revoked_at, reason }), { expirationTtl: remainingTtl });
237
+ return { success: true, attestation };
238
+ }
239
+ catch (error) {
240
+ console.error('Failed to revoke attestation:', error);
241
+ return { success: false, error: 'Internal server error' };
242
+ }
243
+ }
244
+ /**
245
+ * Verify an attestation JWT token.
246
+ *
247
+ * Checks:
248
+ * 1. JWT signature and expiration (cryptographic)
249
+ * 2. Token type is 'botcha-attestation'
250
+ * 3. Revocation status (via KV, fail-open)
251
+ *
252
+ * Returns the parsed attestation payload if valid.
253
+ */
254
+ export async function verifyAttestationToken(sessions, token, secret) {
255
+ try {
256
+ const encoder = new TextEncoder();
257
+ const secretKey = encoder.encode(secret);
258
+ const { payload } = await jwtVerify(token, secretKey, {
259
+ algorithms: ['HS256'],
260
+ });
261
+ // Check token type
262
+ if (payload.type !== 'botcha-attestation') {
263
+ return { valid: false, error: 'Invalid token type. Expected attestation token.' };
264
+ }
265
+ const jti = payload.jti;
266
+ // Check revocation (fail-open)
267
+ if (jti) {
268
+ try {
269
+ const revoked = await sessions.get(`attestation_revoked:${jti}`);
270
+ if (revoked) {
271
+ return { valid: false, error: 'Attestation has been revoked' };
272
+ }
273
+ }
274
+ catch (error) {
275
+ console.error('Failed to check attestation revocation:', error);
276
+ // Fail-open
277
+ }
278
+ }
279
+ // Build typed payload
280
+ const attestationPayload = {
281
+ sub: payload.sub || '',
282
+ iss: payload.iss || '',
283
+ type: 'botcha-attestation',
284
+ jti: jti || '',
285
+ iat: payload.iat || 0,
286
+ exp: payload.exp || 0,
287
+ can: payload.can || [],
288
+ cannot: payload.cannot || [],
289
+ restrictions: payload.restrictions,
290
+ delegation_id: payload.delegation_id,
291
+ metadata: payload.metadata,
292
+ };
293
+ return { valid: true, payload: attestationPayload };
294
+ }
295
+ catch (error) {
296
+ return {
297
+ valid: false,
298
+ error: error instanceof Error ? error.message : 'Invalid attestation token',
299
+ };
300
+ }
301
+ }
302
+ /**
303
+ * Full capability check: verify attestation token + check specific action:resource.
304
+ *
305
+ * Combines token verification with permission checking in one call.
306
+ */
307
+ export async function verifyAndCheckCapability(sessions, token, secret, action, resource) {
308
+ // First verify the token
309
+ const verification = await verifyAttestationToken(sessions, token, secret);
310
+ if (!verification.valid || !verification.payload) {
311
+ return {
312
+ allowed: false,
313
+ error: verification.error || 'Invalid attestation token',
314
+ };
315
+ }
316
+ const payload = verification.payload;
317
+ // Check restrictions if applicable
318
+ if (payload.restrictions?.rate_limit !== undefined) {
319
+ // Rate limit would need a counter — for now, we just pass the restriction through
320
+ // Future: implement per-attestation rate limit counters in KV
321
+ }
322
+ // Check capability
323
+ const check = checkCapability(payload.can, payload.cannot, action, resource);
324
+ return {
325
+ allowed: check.allowed,
326
+ agent_id: payload.sub,
327
+ reason: check.reason,
328
+ matched_rule: check.matched_rule,
329
+ };
330
+ }
331
+ // ============ ENFORCEMENT MIDDLEWARE ============
332
+ /**
333
+ * Create a Hono middleware that enforces capability attestation.
334
+ *
335
+ * Usage:
336
+ * app.get('/api/invoices', requireCapability('read:invoices'), handler);
337
+ * app.post('/api/transfers', requireCapability('write:transfers'), handler);
338
+ *
339
+ * Extracts attestation token from:
340
+ * 1. X-Botcha-Attestation header
341
+ * 2. Authorization: Bearer header (if token type is attestation)
342
+ *
343
+ * On failure: returns 403 with capability denial details.
344
+ * On missing token: returns 401 requesting attestation.
345
+ */
346
+ export function requireCapability(capability) {
347
+ return async (c, next) => {
348
+ // Extract attestation token
349
+ const attestationHeader = c.req.header('x-botcha-attestation');
350
+ const authHeader = c.req.header('authorization');
351
+ const token = attestationHeader || extractBearer(authHeader);
352
+ if (!token) {
353
+ return c.json({
354
+ success: false,
355
+ error: 'ATTESTATION_REQUIRED',
356
+ message: 'Capability attestation token required',
357
+ required_capability: capability,
358
+ hint: 'Include X-Botcha-Attestation header or Authorization: Bearer with attestation token',
359
+ }, 401);
360
+ }
361
+ // Verify and check
362
+ const [action, resource] = normalizeCapability(capability).split(':', 2);
363
+ const result = await verifyAndCheckCapability(c.env.SESSIONS, token, c.env.JWT_SECRET, action, resource === '*' ? undefined : resource);
364
+ if (!result.allowed) {
365
+ return c.json({
366
+ success: false,
367
+ error: 'CAPABILITY_DENIED',
368
+ message: result.reason || result.error || 'Capability check failed',
369
+ required_capability: capability,
370
+ agent_id: result.agent_id,
371
+ matched_rule: result.matched_rule,
372
+ }, 403);
373
+ }
374
+ // Attach attestation info to context for downstream handlers
375
+ c.set('attestation_agent_id', result.agent_id);
376
+ c.set('attestation_capability', capability);
377
+ c.set('attestation_matched_rule', result.matched_rule);
378
+ await next();
379
+ };
380
+ }
381
+ // ============ UTILITY FUNCTIONS ============
382
+ function extractBearer(header) {
383
+ if (!header)
384
+ return null;
385
+ const match = header.match(/^Bearer\s+(.+)$/i);
386
+ return match ? match[1] : null;
387
+ }
388
+ async function updateAttestationIndex(sessions, agentId, attestationId, operation) {
389
+ try {
390
+ const key = `agent_attestations:${agentId}`;
391
+ const data = await sessions.get(key, 'text');
392
+ let ids = data ? JSON.parse(data) : [];
393
+ if (operation === 'add' && !ids.includes(attestationId)) {
394
+ ids.push(attestationId);
395
+ }
396
+ else if (operation === 'remove') {
397
+ ids = ids.filter(id => id !== attestationId);
398
+ }
399
+ await sessions.put(key, JSON.stringify(ids));
400
+ }
401
+ catch (error) {
402
+ console.error('Failed to update attestation index:', error);
403
+ }
404
+ }
405
+ export default {
406
+ issueAttestation,
407
+ getAttestation,
408
+ revokeAttestation,
409
+ verifyAttestationToken,
410
+ verifyAndCheckCapability,
411
+ checkCapability,
412
+ matchesPattern,
413
+ normalizeCapability,
414
+ isValidCapabilityPattern,
415
+ requireCapability,
416
+ };