@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.
- package/dist/dashboard/landing.d.ts.map +1 -1
- package/dist/dashboard/landing.js +2 -9
- package/dist/dashboard/layout.d.ts +12 -0
- package/dist/dashboard/layout.d.ts.map +1 -1
- package/dist/dashboard/layout.js +12 -5
- package/dist/dashboard/showcase.d.ts +1 -0
- package/dist/dashboard/showcase.d.ts.map +1 -1
- package/dist/dashboard/showcase.js +3 -2
- package/dist/dashboard/whitepaper.d.ts +14 -0
- package/dist/dashboard/whitepaper.d.ts.map +1 -0
- package/dist/dashboard/whitepaper.js +418 -0
- package/dist/email.d.ts.map +1 -1
- package/dist/email.js +5 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +148 -18
- package/dist/og-image.d.ts +2 -0
- package/dist/og-image.d.ts.map +1 -0
- package/dist/og-image.js +2 -0
- package/dist/static.d.ts +871 -2
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +812 -4
- package/dist/tap-agents.d.ts +3 -2
- package/dist/tap-agents.d.ts.map +1 -1
- package/dist/tap-agents.js +19 -6
- package/dist/tap-attestation-routes.d.ts +204 -0
- package/dist/tap-attestation-routes.d.ts.map +1 -0
- package/dist/tap-attestation-routes.js +396 -0
- package/dist/tap-attestation.d.ts +178 -0
- package/dist/tap-attestation.d.ts.map +1 -0
- package/dist/tap-attestation.js +416 -0
- package/dist/tap-consumer.d.ts +151 -0
- package/dist/tap-consumer.d.ts.map +1 -0
- package/dist/tap-consumer.js +346 -0
- package/dist/tap-delegation-routes.d.ts +236 -0
- package/dist/tap-delegation-routes.d.ts.map +1 -0
- package/dist/tap-delegation-routes.js +378 -0
- package/dist/tap-delegation.d.ts +127 -0
- package/dist/tap-delegation.d.ts.map +1 -0
- package/dist/tap-delegation.js +490 -0
- package/dist/tap-edge.d.ts +106 -0
- package/dist/tap-edge.d.ts.map +1 -0
- package/dist/tap-edge.js +487 -0
- package/dist/tap-federation.d.ts +89 -0
- package/dist/tap-federation.d.ts.map +1 -0
- package/dist/tap-federation.js +237 -0
- package/dist/tap-jwks.d.ts +64 -0
- package/dist/tap-jwks.d.ts.map +1 -0
- package/dist/tap-jwks.js +279 -0
- package/dist/tap-payment.d.ts +172 -0
- package/dist/tap-payment.d.ts.map +1 -0
- package/dist/tap-payment.js +425 -0
- package/dist/tap-reputation-routes.d.ts +154 -0
- package/dist/tap-reputation-routes.d.ts.map +1 -0
- package/dist/tap-reputation-routes.js +341 -0
- package/dist/tap-reputation.d.ts +136 -0
- package/dist/tap-reputation.d.ts.map +1 -0
- package/dist/tap-reputation.js +346 -0
- package/dist/tap-routes.d.ts +239 -2
- package/dist/tap-routes.d.ts.map +1 -1
- package/dist/tap-routes.js +279 -4
- package/dist/tap-verify.d.ts +43 -1
- package/dist/tap-verify.d.ts.map +1 -1
- package/dist/tap-verify.js +215 -30
- 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"}
|
package/dist/tap-jwks.js
ADDED
|
@@ -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
|
+
};
|