@dupecom/botcha-cloudflare 0.15.0 → 0.18.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 (65) hide show
  1. package/dist/dashboard/landing.d.ts.map +1 -1
  2. package/dist/dashboard/landing.js +2 -9
  3. package/dist/dashboard/layout.d.ts +12 -0
  4. package/dist/dashboard/layout.d.ts.map +1 -1
  5. package/dist/dashboard/layout.js +12 -5
  6. package/dist/dashboard/showcase.d.ts +1 -0
  7. package/dist/dashboard/showcase.d.ts.map +1 -1
  8. package/dist/dashboard/showcase.js +3 -2
  9. package/dist/dashboard/whitepaper.d.ts +14 -0
  10. package/dist/dashboard/whitepaper.d.ts.map +1 -0
  11. package/dist/dashboard/whitepaper.js +418 -0
  12. package/dist/email.d.ts.map +1 -1
  13. package/dist/email.js +5 -1
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +148 -18
  17. package/dist/og-image.d.ts +2 -0
  18. package/dist/og-image.d.ts.map +1 -0
  19. package/dist/og-image.js +2 -0
  20. package/dist/static.d.ts +871 -2
  21. package/dist/static.d.ts.map +1 -1
  22. package/dist/static.js +812 -4
  23. package/dist/tap-agents.d.ts +3 -2
  24. package/dist/tap-agents.d.ts.map +1 -1
  25. package/dist/tap-agents.js +19 -6
  26. package/dist/tap-attestation-routes.d.ts +204 -0
  27. package/dist/tap-attestation-routes.d.ts.map +1 -0
  28. package/dist/tap-attestation-routes.js +396 -0
  29. package/dist/tap-attestation.d.ts +178 -0
  30. package/dist/tap-attestation.d.ts.map +1 -0
  31. package/dist/tap-attestation.js +416 -0
  32. package/dist/tap-consumer.d.ts +151 -0
  33. package/dist/tap-consumer.d.ts.map +1 -0
  34. package/dist/tap-consumer.js +346 -0
  35. package/dist/tap-delegation-routes.d.ts +236 -0
  36. package/dist/tap-delegation-routes.d.ts.map +1 -0
  37. package/dist/tap-delegation-routes.js +378 -0
  38. package/dist/tap-delegation.d.ts +127 -0
  39. package/dist/tap-delegation.d.ts.map +1 -0
  40. package/dist/tap-delegation.js +490 -0
  41. package/dist/tap-edge.d.ts +106 -0
  42. package/dist/tap-edge.d.ts.map +1 -0
  43. package/dist/tap-edge.js +487 -0
  44. package/dist/tap-federation.d.ts +89 -0
  45. package/dist/tap-federation.d.ts.map +1 -0
  46. package/dist/tap-federation.js +237 -0
  47. package/dist/tap-jwks.d.ts +64 -0
  48. package/dist/tap-jwks.d.ts.map +1 -0
  49. package/dist/tap-jwks.js +279 -0
  50. package/dist/tap-payment.d.ts +172 -0
  51. package/dist/tap-payment.d.ts.map +1 -0
  52. package/dist/tap-payment.js +425 -0
  53. package/dist/tap-reputation-routes.d.ts +154 -0
  54. package/dist/tap-reputation-routes.d.ts.map +1 -0
  55. package/dist/tap-reputation-routes.js +341 -0
  56. package/dist/tap-reputation.d.ts +136 -0
  57. package/dist/tap-reputation.d.ts.map +1 -0
  58. package/dist/tap-reputation.js +346 -0
  59. package/dist/tap-routes.d.ts +239 -2
  60. package/dist/tap-routes.d.ts.map +1 -1
  61. package/dist/tap-routes.js +279 -4
  62. package/dist/tap-verify.d.ts +43 -1
  63. package/dist/tap-verify.d.ts.map +1 -1
  64. package/dist/tap-verify.js +215 -30
  65. package/package.json +1 -1
@@ -0,0 +1,237 @@
1
+ /**
2
+ * TAP Federation — External JWKS Federation for Cross-Platform Trust
3
+ * Enables BOTCHA to verify agents signed by Visa or other TAP-compatible providers
4
+ * Per Visa TAP spec: https://developer.visa.com/capabilities/trusted-agent-protocol
5
+ */
6
+ // ============ WELL-KNOWN SOURCES ============
7
+ export const WELL_KNOWN_SOURCES = [
8
+ {
9
+ url: 'https://mcp.visa.com/.well-known/jwks',
10
+ name: 'visa',
11
+ trustLevel: 'high',
12
+ refreshInterval: 3600, // 1 hour
13
+ enabled: true,
14
+ },
15
+ // Future: other payment schemes, agent providers, etc.
16
+ ];
17
+ // ============ CORE FUNCTIONS ============
18
+ /**
19
+ * Fetch JWKS from a URL
20
+ * @throws Error if fetch fails or response is invalid
21
+ */
22
+ export async function fetchJWKS(url) {
23
+ const response = await fetch(url, {
24
+ headers: { 'Accept': 'application/json' },
25
+ // CF Workers: no timeout needed, runtime handles it
26
+ });
27
+ if (!response.ok) {
28
+ throw new Error(`JWKS fetch failed: ${response.status} ${response.statusText}`);
29
+ }
30
+ const jwks = await response.json();
31
+ if (!jwks.keys || !Array.isArray(jwks.keys)) {
32
+ throw new Error('Invalid JWKS: missing keys array');
33
+ }
34
+ return jwks;
35
+ }
36
+ /**
37
+ * Convert a JWK from an external source to FederatedKey format
38
+ */
39
+ export async function jwkToFederatedKey(jwk, source) {
40
+ // Determine the algorithm params for import
41
+ const importParams = resolveImportParams(jwk);
42
+ // Import the JWK into Web Crypto
43
+ const cryptoKey = await crypto.subtle.importKey('jwk', jwk, importParams, true, ['verify']);
44
+ // Export as SPKI to get PEM
45
+ const spkiBuffer = await crypto.subtle.exportKey('spki', cryptoKey);
46
+ const publicKeyPem = arrayBufferToPem(spkiBuffer);
47
+ return {
48
+ kid: jwk.kid,
49
+ kty: jwk.kty,
50
+ alg: jwk.alg || inferAlgorithm(jwk),
51
+ publicKeyPem,
52
+ source: source.name,
53
+ sourceUrl: source.url,
54
+ trustLevel: source.trustLevel,
55
+ fetchedAt: Date.now(),
56
+ expiresAt: Date.now() + source.refreshInterval * 1000,
57
+ x5c: jwk.x5c,
58
+ };
59
+ }
60
+ /**
61
+ * Resolve Web Crypto import parameters based on JWK type
62
+ */
63
+ export function resolveImportParams(jwk) {
64
+ const kty = jwk.kty;
65
+ const alg = jwk.alg;
66
+ if (kty === 'RSA') {
67
+ return { name: 'RSA-PSS', hash: 'SHA-256' };
68
+ }
69
+ if (kty === 'EC') {
70
+ return { name: 'ECDSA', namedCurve: jwk.crv || 'P-256' };
71
+ }
72
+ if (kty === 'OKP' && (jwk.crv === 'Ed25519' || alg === 'EdDSA')) {
73
+ return { name: 'Ed25519' };
74
+ }
75
+ throw new Error(`Unsupported key type: ${kty}`);
76
+ }
77
+ /**
78
+ * Infer algorithm from JWK if not specified
79
+ */
80
+ export function inferAlgorithm(jwk) {
81
+ if (jwk.kty === 'RSA')
82
+ return 'PS256';
83
+ if (jwk.kty === 'EC' && jwk.crv === 'P-256')
84
+ return 'ES256';
85
+ if (jwk.kty === 'OKP' && jwk.crv === 'Ed25519')
86
+ return 'EdDSA';
87
+ return 'unknown';
88
+ }
89
+ /**
90
+ * Convert ArrayBuffer to PEM string
91
+ */
92
+ function arrayBufferToPem(buffer) {
93
+ const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
94
+ const lines = base64.match(/.{1,64}/g) || [base64];
95
+ return `-----BEGIN PUBLIC KEY-----\n${lines.join('\n')}\n-----END PUBLIC KEY-----`;
96
+ }
97
+ /**
98
+ * Create a federation resolver that fetches and caches keys from external sources
99
+ */
100
+ export function createFederationResolver(config) {
101
+ // In-memory cache (per-isolate)
102
+ const memoryCache = new Map();
103
+ return {
104
+ /**
105
+ * Resolve a public key by kid
106
+ * Search order: memory cache → KV cache → fetch from sources
107
+ */
108
+ async resolveKey(kid) {
109
+ // 1. Check memory cache
110
+ const cached = memoryCache.get(kid);
111
+ if (cached && Date.now() < cached.expiresAt) {
112
+ return { found: true, key: cached };
113
+ }
114
+ // 2. Check KV cache
115
+ if (config.kvNamespace) {
116
+ try {
117
+ const kvData = await config.kvNamespace.get(`federated_key:${kid}`, 'text');
118
+ if (kvData) {
119
+ const key = JSON.parse(kvData);
120
+ if (Date.now() < key.expiresAt) {
121
+ memoryCache.set(kid, key);
122
+ return { found: true, key };
123
+ }
124
+ }
125
+ }
126
+ catch (error) {
127
+ console.error('Federation: KV cache read error:', error);
128
+ // Continue to fetch from sources
129
+ }
130
+ }
131
+ // 3. Fetch from all enabled sources
132
+ for (const source of config.sources.filter(s => s.enabled)) {
133
+ try {
134
+ const jwks = await fetchJWKS(source.url);
135
+ // Opportunistically cache ALL keys from this fetch
136
+ let foundKey;
137
+ for (const jwk of jwks.keys) {
138
+ try {
139
+ const key = await jwkToFederatedKey(jwk, source);
140
+ memoryCache.set(jwk.kid, key);
141
+ // Cache in KV
142
+ if (config.kvNamespace) {
143
+ try {
144
+ const ttl = Math.min(source.refreshInterval, config.maxCacheAge || 86400);
145
+ await config.kvNamespace.put(`federated_key:${jwk.kid}`, JSON.stringify(key), { expirationTtl: ttl });
146
+ }
147
+ catch { /* skip write errors */ }
148
+ }
149
+ // Check if this is the key we're looking for
150
+ if (jwk.kid === kid) {
151
+ foundKey = key;
152
+ }
153
+ }
154
+ catch { /* skip invalid keys */ }
155
+ }
156
+ // Return if we found the key in this source
157
+ if (foundKey) {
158
+ return { found: true, key: foundKey };
159
+ }
160
+ }
161
+ catch (error) {
162
+ console.error(`Federation: Failed to fetch from ${source.name}:`, error);
163
+ // Continue to next source (fail-open per BOTCHA philosophy)
164
+ }
165
+ }
166
+ return { found: false, error: `Key ${kid} not found in any federated source` };
167
+ },
168
+ /**
169
+ * Refresh all keys from all sources (background job)
170
+ */
171
+ async refreshAll() {
172
+ let refreshed = 0;
173
+ const errors = [];
174
+ for (const source of config.sources.filter(s => s.enabled)) {
175
+ try {
176
+ const jwks = await fetchJWKS(source.url);
177
+ for (const jwk of jwks.keys) {
178
+ try {
179
+ const key = await jwkToFederatedKey(jwk, source);
180
+ memoryCache.set(jwk.kid, key);
181
+ if (config.kvNamespace) {
182
+ try {
183
+ await config.kvNamespace.put(`federated_key:${jwk.kid}`, JSON.stringify(key), { expirationTtl: source.refreshInterval });
184
+ }
185
+ catch (error) {
186
+ // Non-fatal
187
+ }
188
+ }
189
+ refreshed++;
190
+ }
191
+ catch (e) {
192
+ errors.push(`Failed to process key ${jwk.kid} from ${source.name}`);
193
+ }
194
+ }
195
+ }
196
+ catch (e) {
197
+ errors.push(`Failed to fetch ${source.name}: ${e}`);
198
+ }
199
+ }
200
+ return { refreshed, errors };
201
+ },
202
+ /**
203
+ * Get all cached keys (for debugging/admin)
204
+ */
205
+ getCachedKeys() {
206
+ return Array.from(memoryCache.values());
207
+ },
208
+ /**
209
+ * Clear all caches
210
+ */
211
+ clearCache() {
212
+ memoryCache.clear();
213
+ },
214
+ };
215
+ }
216
+ // ============ CONVENIENCE EXPORTS ============
217
+ /**
218
+ * Create a Visa-specific federation resolver
219
+ */
220
+ export function createVisaFederationResolver(kvNamespace) {
221
+ return createFederationResolver({
222
+ sources: WELL_KNOWN_SOURCES,
223
+ kvNamespace,
224
+ defaultRefreshInterval: 3600,
225
+ maxCacheAge: 86400,
226
+ });
227
+ }
228
+ // ============ DEFAULT EXPORT ============
229
+ export default {
230
+ fetchJWKS,
231
+ jwkToFederatedKey,
232
+ createFederationResolver,
233
+ createVisaFederationResolver,
234
+ resolveImportParams,
235
+ inferAlgorithm,
236
+ WELL_KNOWN_SOURCES,
237
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * TAP JWKS — JWK Set Endpoint + Key Format Conversion
3
+ * Implements .well-known/jwks for TAP agent public key discovery
4
+ * Per Visa TAP spec: https://developer.visa.com/capabilities/trusted-agent-protocol
5
+ */
6
+ import type { Context } from 'hono';
7
+ export interface JWK {
8
+ kty: string;
9
+ kid: string;
10
+ use: string;
11
+ alg: string;
12
+ n?: string;
13
+ e?: string;
14
+ crv?: string;
15
+ x?: string;
16
+ y?: string;
17
+ agent_id?: string;
18
+ agent_name?: string;
19
+ expires_at?: string;
20
+ }
21
+ export interface JWKSet {
22
+ keys: JWK[];
23
+ }
24
+ /**
25
+ * Convert PEM public key to JWK format
26
+ */
27
+ export declare function pemToJwk(pem: string, algorithm: string, kid: string, metadata?: {
28
+ agent_id?: string;
29
+ agent_name?: string;
30
+ expires_at?: string;
31
+ }): Promise<JWK>;
32
+ /**
33
+ * Convert JWK back to PEM format (for verification)
34
+ */
35
+ export declare function jwkToPem(jwk: JWK): Promise<string>;
36
+ /**
37
+ * Map BOTCHA algorithm names to JWK algorithm identifiers
38
+ */
39
+ export declare function algToJWKAlg(algorithm: string): string;
40
+ /**
41
+ * GET /.well-known/jwks
42
+ * Returns JWK Set for app's TAP-enabled agents
43
+ */
44
+ export declare function jwksRoute(c: Context): Promise<Response>;
45
+ /**
46
+ * GET /v1/keys/:keyId
47
+ * Get a specific key by ID (agent_id)
48
+ */
49
+ export declare function getKeyRoute(c: Context): Promise<Response>;
50
+ /**
51
+ * GET /v1/keys
52
+ * List keys with optional filters (Visa TAP compatible)
53
+ */
54
+ export declare function listKeysRoute(c: Context): Promise<Response>;
55
+ declare const _default: {
56
+ pemToJwk: typeof pemToJwk;
57
+ jwkToPem: typeof jwkToPem;
58
+ algToJWKAlg: typeof algToJWKAlg;
59
+ jwksRoute: typeof jwksRoute;
60
+ getKeyRoute: typeof getKeyRoute;
61
+ listKeysRoute: typeof listKeysRoute;
62
+ };
63
+ export default _default;
64
+ //# sourceMappingURL=tap-jwks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tap-jwks.d.ts","sourceRoot":"","sources":["../src/tap-jwks.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAMpC,MAAM,WAAW,GAAG;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IAEZ,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IAEX,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IAEX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,GAAG,EAAE,CAAC;CACb;AAID;;GAEG;AACH,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE;IACT,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,OAAO,CAAC,GAAG,CAAC,CAed;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAKxD;AAID;;GAEG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAWrD;AAiFD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CA6E7D;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CA2D/D;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAuBjE;;;;;;;;;AAED,wBAOE"}
@@ -0,0 +1,279 @@
1
+ /**
2
+ * TAP JWKS — JWK Set Endpoint + Key Format Conversion
3
+ * Implements .well-known/jwks for TAP agent public key discovery
4
+ * Per Visa TAP spec: https://developer.visa.com/capabilities/trusted-agent-protocol
5
+ */
6
+ // ============ PEM <-> JWK CONVERSION ============
7
+ /**
8
+ * Convert PEM public key to JWK format
9
+ */
10
+ export async function pemToJwk(pem, algorithm, kid, metadata) {
11
+ const keyData = pemToArrayBuffer(pem);
12
+ const importParams = getImportParamsForAlg(algorithm);
13
+ const cryptoKey = await crypto.subtle.importKey('spki', keyData, importParams, true, ['verify']);
14
+ const jwk = (await crypto.subtle.exportKey('jwk', cryptoKey));
15
+ return {
16
+ ...jwk,
17
+ kid,
18
+ use: 'sig',
19
+ alg: algToJWKAlg(algorithm),
20
+ ...(metadata?.agent_id && { agent_id: metadata.agent_id }),
21
+ ...(metadata?.agent_name && { agent_name: metadata.agent_name }),
22
+ ...(metadata?.expires_at && { expires_at: metadata.expires_at }),
23
+ };
24
+ }
25
+ /**
26
+ * Convert JWK back to PEM format (for verification)
27
+ */
28
+ export async function jwkToPem(jwk) {
29
+ const importParams = jwkAlgToImportParams(jwk.alg);
30
+ const cryptoKey = await crypto.subtle.importKey('jwk', jwk, importParams, true, ['verify']);
31
+ const spkiBuffer = await crypto.subtle.exportKey('spki', cryptoKey);
32
+ return arrayBufferToPem(spkiBuffer);
33
+ }
34
+ // ============ ALGORITHM MAPPING ============
35
+ /**
36
+ * Map BOTCHA algorithm names to JWK algorithm identifiers
37
+ */
38
+ export function algToJWKAlg(algorithm) {
39
+ switch (algorithm) {
40
+ case 'ecdsa-p256-sha256':
41
+ return 'ES256';
42
+ case 'rsa-pss-sha256':
43
+ return 'PS256';
44
+ case 'ed25519':
45
+ return 'EdDSA';
46
+ default:
47
+ throw new Error(`Unsupported algorithm: ${algorithm}`);
48
+ }
49
+ }
50
+ /**
51
+ * Get Web Crypto import parameters for BOTCHA algorithm
52
+ */
53
+ function getImportParamsForAlg(algorithm) {
54
+ switch (algorithm) {
55
+ case 'ecdsa-p256-sha256':
56
+ return {
57
+ name: 'ECDSA',
58
+ namedCurve: 'P-256',
59
+ };
60
+ case 'rsa-pss-sha256':
61
+ return {
62
+ name: 'RSA-PSS',
63
+ hash: 'SHA-256',
64
+ };
65
+ case 'ed25519':
66
+ // Note: Ed25519 support varies by runtime
67
+ // Cloudflare Workers supports it via Web Crypto
68
+ return {
69
+ name: 'Ed25519',
70
+ };
71
+ default:
72
+ throw new Error(`Unsupported algorithm: ${algorithm}`);
73
+ }
74
+ }
75
+ /**
76
+ * Get Web Crypto import parameters for JWK algorithm
77
+ */
78
+ function jwkAlgToImportParams(alg) {
79
+ switch (alg) {
80
+ case 'ES256':
81
+ return {
82
+ name: 'ECDSA',
83
+ namedCurve: 'P-256',
84
+ };
85
+ case 'PS256':
86
+ return {
87
+ name: 'RSA-PSS',
88
+ hash: 'SHA-256',
89
+ };
90
+ case 'EdDSA':
91
+ return {
92
+ name: 'Ed25519',
93
+ };
94
+ default:
95
+ throw new Error(`Unsupported JWK algorithm: ${alg}`);
96
+ }
97
+ }
98
+ // ============ PEM UTILITIES ============
99
+ /**
100
+ * Convert PEM string to ArrayBuffer
101
+ */
102
+ function pemToArrayBuffer(pem) {
103
+ const base64 = pem
104
+ .replace(/-----BEGIN PUBLIC KEY-----/, '')
105
+ .replace(/-----END PUBLIC KEY-----/, '')
106
+ .replace(/\s/g, '');
107
+ const binary = atob(base64);
108
+ const bytes = new Uint8Array(binary.length);
109
+ for (let i = 0; i < binary.length; i++) {
110
+ bytes[i] = binary.charCodeAt(i);
111
+ }
112
+ return bytes.buffer;
113
+ }
114
+ /**
115
+ * Convert ArrayBuffer to PEM string
116
+ */
117
+ function arrayBufferToPem(buffer) {
118
+ const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
119
+ const lines = base64.match(/.{1,64}/g) || [base64];
120
+ return `-----BEGIN PUBLIC KEY-----\n${lines.join('\n')}\n-----END PUBLIC KEY-----`;
121
+ }
122
+ // ============ JWKS ENDPOINT HANDLERS ============
123
+ /**
124
+ * GET /.well-known/jwks
125
+ * Returns JWK Set for app's TAP-enabled agents
126
+ */
127
+ export async function jwksRoute(c) {
128
+ try {
129
+ const appId = c.req.query('app_id');
130
+ // Security: Don't expose all keys globally
131
+ if (!appId) {
132
+ return c.json({ keys: [] }, 200, {
133
+ 'Cache-Control': 'public, max-age=3600',
134
+ });
135
+ }
136
+ const agents = c.env.AGENTS;
137
+ if (!agents) {
138
+ console.error('AGENTS KV namespace not available');
139
+ return c.json({ keys: [] }, 200);
140
+ }
141
+ // Get agent list for this app
142
+ const agentIndexKey = `app_agents:${appId}`;
143
+ const agentIdsData = await agents.get(agentIndexKey, 'text');
144
+ if (!agentIdsData) {
145
+ return c.json({ keys: [] }, 200, {
146
+ 'Cache-Control': 'public, max-age=3600',
147
+ });
148
+ }
149
+ const agentIds = JSON.parse(agentIdsData);
150
+ // Fetch all agents in parallel
151
+ const agentPromises = agentIds.map(async (agentId) => {
152
+ const agentData = await agents.get(`agent:${agentId}`, 'text');
153
+ return agentData ? JSON.parse(agentData) : null;
154
+ });
155
+ const agentResults = await Promise.all(agentPromises);
156
+ // Filter to TAP-enabled agents with public keys
157
+ const tapAgents = agentResults.filter((agent) => agent !== null &&
158
+ agent.tap_enabled === true &&
159
+ Boolean(agent.public_key) &&
160
+ Boolean(agent.signature_algorithm));
161
+ // Convert PEM keys to JWK format
162
+ const jwkPromises = tapAgents.map(async (agent) => {
163
+ try {
164
+ return await pemToJwk(agent.public_key, agent.signature_algorithm, agent.agent_id, {
165
+ agent_id: agent.agent_id,
166
+ agent_name: agent.name,
167
+ expires_at: agent.key_created_at
168
+ ? new Date(agent.key_created_at + 31536000000).toISOString() // +1 year
169
+ : undefined,
170
+ });
171
+ }
172
+ catch (error) {
173
+ console.error(`Failed to convert key for agent ${agent.agent_id}:`, error);
174
+ return null;
175
+ }
176
+ });
177
+ const jwks = (await Promise.all(jwkPromises)).filter((jwk) => jwk !== null);
178
+ return c.json({ keys: jwks }, 200, {
179
+ 'Cache-Control': 'public, max-age=3600',
180
+ });
181
+ }
182
+ catch (error) {
183
+ console.error('JWKS endpoint error:', error);
184
+ // Fail-open: Return empty key set
185
+ return c.json({ keys: [] }, 200);
186
+ }
187
+ }
188
+ /**
189
+ * GET /v1/keys/:keyId
190
+ * Get a specific key by ID (agent_id)
191
+ */
192
+ export async function getKeyRoute(c) {
193
+ try {
194
+ const keyId = c.req.param('keyId') || c.req.query('keyID');
195
+ if (!keyId) {
196
+ return c.json({ error: 'keyId or keyID parameter required' }, 400);
197
+ }
198
+ const agents = c.env.AGENTS;
199
+ if (!agents) {
200
+ console.error('AGENTS KV namespace not available');
201
+ return c.json({ error: 'Service unavailable' }, 503);
202
+ }
203
+ // Get agent by ID
204
+ const agentData = await agents.get(`agent:${keyId}`, 'text');
205
+ if (!agentData) {
206
+ return c.json({ error: 'Key not found' }, 404);
207
+ }
208
+ const agent = JSON.parse(agentData);
209
+ // Verify agent has TAP enabled and public key
210
+ if (!agent.tap_enabled || !agent.public_key || !agent.signature_algorithm) {
211
+ return c.json({ error: 'Key not found' }, 404);
212
+ }
213
+ // Convert to JWK — if the stored PEM is invalid, return a raw key stub
214
+ let jwk;
215
+ try {
216
+ jwk = await pemToJwk(agent.public_key, agent.signature_algorithm, agent.agent_id, {
217
+ agent_id: agent.agent_id,
218
+ agent_name: agent.name,
219
+ expires_at: agent.key_created_at
220
+ ? new Date(agent.key_created_at + 31536000000).toISOString()
221
+ : undefined,
222
+ });
223
+ }
224
+ catch (conversionError) {
225
+ // PEM is stored but can't be converted to JWK (e.g., invalid key material)
226
+ // Return a raw stub so the endpoint doesn't 500
227
+ console.warn('Failed to convert agent key to JWK:', conversionError);
228
+ jwk = {
229
+ kty: agent.signature_algorithm === 'ed25519' ? 'OKP' : 'EC',
230
+ kid: agent.agent_id,
231
+ alg: algToJWKAlg(agent.signature_algorithm),
232
+ use: 'sig',
233
+ raw_pem: agent.public_key,
234
+ error: 'Key material could not be converted to JWK format',
235
+ };
236
+ }
237
+ return c.json(jwk, 200, {
238
+ 'Cache-Control': 'public, max-age=3600',
239
+ });
240
+ }
241
+ catch (error) {
242
+ console.error('Get key error:', error);
243
+ return c.json({ error: 'Internal server error' }, 500);
244
+ }
245
+ }
246
+ /**
247
+ * GET /v1/keys
248
+ * List keys with optional filters (Visa TAP compatible)
249
+ */
250
+ export async function listKeysRoute(c) {
251
+ try {
252
+ const keyID = c.req.query('keyID');
253
+ const appId = c.req.query('app_id');
254
+ // If keyID provided, return single key (Visa TAP compat)
255
+ if (keyID) {
256
+ return getKeyRoute(c);
257
+ }
258
+ // If app_id provided, return all keys for app (same as jwksRoute)
259
+ if (appId) {
260
+ return jwksRoute(c);
261
+ }
262
+ // No filters: return empty set (don't expose all keys)
263
+ return c.json({ keys: [] }, 200, {
264
+ 'Cache-Control': 'public, max-age=3600',
265
+ });
266
+ }
267
+ catch (error) {
268
+ console.error('List keys error:', error);
269
+ return c.json({ keys: [] }, 200);
270
+ }
271
+ }
272
+ export default {
273
+ pemToJwk,
274
+ jwkToPem,
275
+ algToJWKAlg,
276
+ jwksRoute,
277
+ getKeyRoute,
278
+ listKeysRoute,
279
+ };