@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,106 @@
1
+ /**
2
+ * TAP Edge Verification — CDN-layer TAP signature verification
3
+ *
4
+ * Drop-in Hono middleware for Cloudflare Workers that:
5
+ * 1. Intercepts requests with TAP signature headers
6
+ * 2. Fetches agent public keys from BOTCHA or Visa JWKS
7
+ * 3. Verifies RFC 9421 signatures
8
+ * 4. Adds verification result headers to the proxied request
9
+ * 5. Passes through non-TAP requests unmodified
10
+ *
11
+ * Usage:
12
+ * ```typescript
13
+ * import { createTAPEdgeMiddleware } from '@dupecom/botcha/edge';
14
+ *
15
+ * app.use('*', createTAPEdgeMiddleware({
16
+ * jwksUrls: ['https://botcha.ai/.well-known/jwks?app_id=YOUR_APP'],
17
+ * allowUnverified: false, // Block unsigned requests
18
+ * requireTag: true, // Require agent-browser-auth or agent-payer-auth tag
19
+ * }));
20
+ * ```
21
+ */
22
+ import type { MiddlewareHandler } from 'hono';
23
+ export interface TAPEdgeOptions {
24
+ jwksUrls?: string[];
25
+ staticKeys?: Map<string, string>;
26
+ allowUnverified?: boolean;
27
+ requireTag?: boolean;
28
+ blockOnFailure?: boolean;
29
+ keyCacheTtl?: number;
30
+ onVerified?: (result: EdgeVerificationResult) => void;
31
+ onFailed?: (result: EdgeVerificationResult) => void;
32
+ }
33
+ export interface EdgeVerificationResult {
34
+ verified: boolean;
35
+ tag?: string;
36
+ agentKeyId?: string;
37
+ algorithm?: string;
38
+ nonce?: string;
39
+ timestamp?: number;
40
+ error?: string;
41
+ source?: 'botcha' | 'visa' | 'static' | 'unknown';
42
+ }
43
+ export interface ParsedEdgeSignatureInput {
44
+ label: string;
45
+ components: string[];
46
+ created: number;
47
+ expires?: number;
48
+ keyId: string;
49
+ algorithm: string;
50
+ nonce?: string;
51
+ tag?: string;
52
+ }
53
+ export declare const TAP_EDGE_HEADERS: {
54
+ readonly VERIFIED: "X-TAP-Verified";
55
+ readonly TAG: "X-TAP-Tag";
56
+ readonly KEY_ID: "X-TAP-Key-ID";
57
+ readonly AGENT_SOURCE: "X-TAP-Agent-Source";
58
+ readonly NONCE: "X-TAP-Nonce";
59
+ readonly TIMESTAMP: "X-TAP-Timestamp";
60
+ };
61
+ export declare function createTAPEdgeMiddleware(options?: TAPEdgeOptions): MiddlewareHandler;
62
+ /**
63
+ * Parse signature-input header according to RFC 9421
64
+ * Supports BOTH sig1 (BOTCHA) and sig2 (Visa TAP) labels
65
+ */
66
+ export declare function parseEdgeSignatureInput(input: string): ParsedEdgeSignatureInput | null;
67
+ /**
68
+ * Convert JWK to PEM format using Web Crypto
69
+ */
70
+ export declare function jwkToPublicKeyPem(jwk: any): Promise<string>;
71
+ /**
72
+ * Verify edge signature against signature base
73
+ */
74
+ export declare function verifyEdgeSignature(req: any, parsed: ParsedEdgeSignatureInput, signature: string, publicKey: string, algorithm: string): Promise<EdgeVerificationResult>;
75
+ /**
76
+ * Build signature base string according to RFC 9421 TAP format
77
+ */
78
+ export declare function buildEdgeSignatureBase(authority: string, path: string, parsed: ParsedEdgeSignatureInput): string;
79
+ /**
80
+ * Strict mode: require TAP on all requests, block failures
81
+ */
82
+ export declare const tapEdgeStrict: (jwksUrls: string[]) => MiddlewareHandler;
83
+ /**
84
+ * Flexible mode: verify if present, pass through if not
85
+ */
86
+ export declare const tapEdgeFlexible: (jwksUrls: string[]) => MiddlewareHandler;
87
+ /**
88
+ * Development mode: log only, never block
89
+ */
90
+ export declare const tapEdgeDev: () => MiddlewareHandler;
91
+ declare const _default: {
92
+ createTAPEdgeMiddleware: typeof createTAPEdgeMiddleware;
93
+ tapEdgeStrict: (jwksUrls: string[]) => MiddlewareHandler;
94
+ tapEdgeFlexible: (jwksUrls: string[]) => MiddlewareHandler;
95
+ tapEdgeDev: () => MiddlewareHandler;
96
+ TAP_EDGE_HEADERS: {
97
+ readonly VERIFIED: "X-TAP-Verified";
98
+ readonly TAG: "X-TAP-Tag";
99
+ readonly KEY_ID: "X-TAP-Key-ID";
100
+ readonly AGENT_SOURCE: "X-TAP-Agent-Source";
101
+ readonly NONCE: "X-TAP-Nonce";
102
+ readonly TIMESTAMP: "X-TAP-Timestamp";
103
+ };
104
+ };
105
+ export default _default;
106
+ //# sourceMappingURL=tap-edge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tap-edge.d.ts","sourceRoot":"","sources":["../src/tap-edge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAW,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAIvD,MAAM,WAAW,cAAc;IAE7B,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAGjC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,cAAc,CAAC,EAAE,OAAO,CAAC;IAGzB,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,CAAC;IACtD,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,OAAO,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;CACnD;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAcD,eAAO,MAAM,gBAAgB;;;;;;;CAOnB,CAAC;AAIX,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,cAAmB,GAAG,iBAAiB,CAoFvF;AAID;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,wBAAwB,GAAG,IAAI,CAkCtF;AA6CD;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAOjE;AA0BD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,wBAAwB,EAChC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,sBAAsB,CAAC,CA8DjC;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,wBAAwB,GAC/B,MAAM,CAgCR;AAsMD;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,UAAU,MAAM,EAAE,sBAK9C,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,UAAU,MAAM,EAAE,sBAKhD,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,UAAU,yBAKrB,CAAC;;;8BAzBqC,MAAM,EAAE;gCAUN,MAAM,EAAE;;;;;;;;;;;AAmBlD,wBAME"}
@@ -0,0 +1,487 @@
1
+ /**
2
+ * TAP Edge Verification — CDN-layer TAP signature verification
3
+ *
4
+ * Drop-in Hono middleware for Cloudflare Workers that:
5
+ * 1. Intercepts requests with TAP signature headers
6
+ * 2. Fetches agent public keys from BOTCHA or Visa JWKS
7
+ * 3. Verifies RFC 9421 signatures
8
+ * 4. Adds verification result headers to the proxied request
9
+ * 5. Passes through non-TAP requests unmodified
10
+ *
11
+ * Usage:
12
+ * ```typescript
13
+ * import { createTAPEdgeMiddleware } from '@dupecom/botcha/edge';
14
+ *
15
+ * app.use('*', createTAPEdgeMiddleware({
16
+ * jwksUrls: ['https://botcha.ai/.well-known/jwks?app_id=YOUR_APP'],
17
+ * allowUnverified: false, // Block unsigned requests
18
+ * requireTag: true, // Require agent-browser-auth or agent-payer-auth tag
19
+ * }));
20
+ * ```
21
+ */
22
+ // Headers added to proxied request after verification
23
+ export const TAP_EDGE_HEADERS = {
24
+ VERIFIED: 'X-TAP-Verified', // 'true' or 'false'
25
+ TAG: 'X-TAP-Tag', // 'agent-browser-auth' or 'agent-payer-auth'
26
+ KEY_ID: 'X-TAP-Key-ID', // Which key was used
27
+ AGENT_SOURCE: 'X-TAP-Agent-Source', // 'botcha' | 'visa' | 'static'
28
+ NONCE: 'X-TAP-Nonce', // For body object linking downstream
29
+ TIMESTAMP: 'X-TAP-Timestamp', // Signature creation time
30
+ };
31
+ // ============ MAIN MIDDLEWARE ============
32
+ export function createTAPEdgeMiddleware(options = {}) {
33
+ const { jwksUrls = [], staticKeys = new Map(), allowUnverified = true, requireTag = false, blockOnFailure = true, keyCacheTtl = 3600, onVerified, onFailed, } = options;
34
+ // In-memory key cache (per-isolate in CF Workers)
35
+ const keyCache = new Map();
36
+ return async (c, next) => {
37
+ // 1. Check for TAP signature headers
38
+ const signatureInput = c.req.header('signature-input');
39
+ const signature = c.req.header('signature');
40
+ if (!signatureInput || !signature) {
41
+ if (!allowUnverified) {
42
+ return c.json({ error: 'TAP_REQUIRED', message: 'TAP signature headers required' }, 403);
43
+ }
44
+ c.header(TAP_EDGE_HEADERS.VERIFIED, 'false');
45
+ await next();
46
+ return;
47
+ }
48
+ // 2. Parse signature input
49
+ const parsed = parseEdgeSignatureInput(signatureInput);
50
+ if (!parsed) {
51
+ const result = { verified: false, error: 'Invalid signature-input format' };
52
+ onFailed?.(result);
53
+ if (blockOnFailure) {
54
+ return c.json({ error: 'TAP_INVALID', message: 'Invalid TAP signature format' }, 403);
55
+ }
56
+ c.header(TAP_EDGE_HEADERS.VERIFIED, 'false');
57
+ await next();
58
+ return;
59
+ }
60
+ // 3. Check tag requirement
61
+ if (requireTag && !parsed.tag) {
62
+ return c.json({ error: 'TAP_TAG_REQUIRED', message: 'TAP tag required (agent-browser-auth or agent-payer-auth)' }, 403);
63
+ }
64
+ // 4. Resolve public key
65
+ const keyResult = await resolveKey(parsed.keyId, jwksUrls, staticKeys, keyCache, keyCacheTtl);
66
+ if (!keyResult) {
67
+ const result = { verified: false, error: 'Public key not found', agentKeyId: parsed.keyId };
68
+ onFailed?.(result);
69
+ if (blockOnFailure) {
70
+ return c.json({ error: 'TAP_KEY_NOT_FOUND', message: `Public key not found for keyId: ${parsed.keyId}` }, 403);
71
+ }
72
+ c.header(TAP_EDGE_HEADERS.VERIFIED, 'false');
73
+ await next();
74
+ return;
75
+ }
76
+ // 5. Verify signature
77
+ const verificationResult = await verifyEdgeSignature(c.req, parsed, signature, keyResult.key, parsed.algorithm);
78
+ if (verificationResult.verified) {
79
+ verificationResult.source = keyResult.source;
80
+ onVerified?.(verificationResult);
81
+ // Add verification headers
82
+ c.header(TAP_EDGE_HEADERS.VERIFIED, 'true');
83
+ if (parsed.tag)
84
+ c.header(TAP_EDGE_HEADERS.TAG, parsed.tag);
85
+ c.header(TAP_EDGE_HEADERS.KEY_ID, parsed.keyId);
86
+ c.header(TAP_EDGE_HEADERS.AGENT_SOURCE, keyResult.source);
87
+ if (parsed.nonce)
88
+ c.header(TAP_EDGE_HEADERS.NONCE, parsed.nonce);
89
+ if (parsed.created)
90
+ c.header(TAP_EDGE_HEADERS.TIMESTAMP, String(parsed.created));
91
+ }
92
+ else {
93
+ onFailed?.(verificationResult);
94
+ if (blockOnFailure) {
95
+ return c.json({ error: 'TAP_VERIFICATION_FAILED', message: verificationResult.error }, 403);
96
+ }
97
+ c.header(TAP_EDGE_HEADERS.VERIFIED, 'false');
98
+ }
99
+ await next();
100
+ };
101
+ }
102
+ // ============ HELPER FUNCTIONS ============
103
+ /**
104
+ * Parse signature-input header according to RFC 9421
105
+ * Supports BOTH sig1 (BOTCHA) and sig2 (Visa TAP) labels
106
+ */
107
+ export function parseEdgeSignatureInput(input) {
108
+ try {
109
+ // Match sig1 OR sig2
110
+ const sigMatch = input.match(/(sig[12])=\(([^)]+)\)/);
111
+ if (!sigMatch)
112
+ return null;
113
+ const label = sigMatch[1];
114
+ const components = sigMatch[2]
115
+ .split(' ')
116
+ .map(h => h.replace(/"/g, ''));
117
+ // Extract all params (keyid/keyId, alg, created, expires, nonce, tag)
118
+ const keyIdMatch = input.match(/keyid="([^"]+)"/i);
119
+ const algMatch = input.match(/alg="([^"]+)"/);
120
+ const createdMatch = input.match(/created=(\d+)/);
121
+ const expiresMatch = input.match(/expires=(\d+)/);
122
+ const nonceMatch = input.match(/nonce="([^"]+)"/);
123
+ const tagMatch = input.match(/tag="([^"]+)"/);
124
+ if (!keyIdMatch || !algMatch || !createdMatch)
125
+ return null;
126
+ return {
127
+ label,
128
+ keyId: keyIdMatch[1],
129
+ algorithm: algMatch[1],
130
+ created: parseInt(createdMatch[1]),
131
+ expires: expiresMatch ? parseInt(expiresMatch[1]) : undefined,
132
+ nonce: nonceMatch ? nonceMatch[1] : undefined,
133
+ tag: tagMatch ? tagMatch[1] : undefined,
134
+ components
135
+ };
136
+ }
137
+ catch {
138
+ return null;
139
+ }
140
+ }
141
+ /**
142
+ * Resolve public key from static keys, cache, or JWKS endpoints
143
+ */
144
+ async function resolveKey(keyId, jwksUrls, staticKeys, cache, cacheTtl) {
145
+ // 1. Check static keys
146
+ if (staticKeys.has(keyId)) {
147
+ return { key: staticKeys.get(keyId), source: 'static' };
148
+ }
149
+ // 2. Check cache
150
+ const cached = cache.get(keyId);
151
+ if (cached && (Date.now() - cached.fetchedAt) / 1000 < cacheTtl) {
152
+ return { key: cached.key, source: cached.source };
153
+ }
154
+ // 3. Fetch from JWKS endpoints
155
+ for (const url of jwksUrls) {
156
+ try {
157
+ const resp = await fetch(url);
158
+ if (!resp.ok)
159
+ continue;
160
+ const jwks = await resp.json();
161
+ const matchingKey = jwks.keys?.find((k) => k.kid === keyId);
162
+ if (matchingKey) {
163
+ const pem = await jwkToPublicKeyPem(matchingKey);
164
+ const source = url.includes('visa.com') ? 'visa' :
165
+ url.includes('botcha') ? 'botcha' : 'unknown';
166
+ cache.set(keyId, { key: pem, fetchedAt: Date.now(), source });
167
+ return { key: pem, source };
168
+ }
169
+ }
170
+ catch (e) {
171
+ console.error(`Failed to fetch JWKS from ${url}:`, e);
172
+ }
173
+ }
174
+ return null;
175
+ }
176
+ /**
177
+ * Convert JWK to PEM format using Web Crypto
178
+ */
179
+ export async function jwkToPublicKeyPem(jwk) {
180
+ const algorithm = jwkAlgToImportParams(jwk.alg || jwk.kty);
181
+ const key = await crypto.subtle.importKey('jwk', jwk, algorithm, true, ['verify']);
182
+ const spki = await crypto.subtle.exportKey('spki', key);
183
+ const base64 = btoa(String.fromCharCode(...new Uint8Array(spki)));
184
+ const lines = base64.match(/.{1,64}/g) || [base64];
185
+ return `-----BEGIN PUBLIC KEY-----\n${lines.join('\n')}\n-----END PUBLIC KEY-----`;
186
+ }
187
+ /**
188
+ * Get Web Crypto import parameters for JWK algorithm
189
+ */
190
+ function jwkAlgToImportParams(alg) {
191
+ switch (alg) {
192
+ case 'ES256':
193
+ return { name: 'ECDSA', namedCurve: 'P-256' };
194
+ case 'PS256':
195
+ return { name: 'RSA-PSS', hash: 'SHA-256' };
196
+ case 'EdDSA':
197
+ return { name: 'Ed25519' };
198
+ case 'EC':
199
+ // Default EC to P-256
200
+ return { name: 'ECDSA', namedCurve: 'P-256' };
201
+ case 'RSA':
202
+ return { name: 'RSA-PSS', hash: 'SHA-256' };
203
+ case 'OKP':
204
+ // Octet Key Pair - Ed25519
205
+ return { name: 'Ed25519' };
206
+ default:
207
+ throw new Error(`Unsupported JWK algorithm: ${alg}`);
208
+ }
209
+ }
210
+ /**
211
+ * Verify edge signature against signature base
212
+ */
213
+ export async function verifyEdgeSignature(req, parsed, signature, publicKey, algorithm) {
214
+ try {
215
+ // 1. Validate timestamps
216
+ const timestampValidation = validateEdgeTimestamps(parsed.created, parsed.expires);
217
+ if (!timestampValidation.valid) {
218
+ return { verified: false, error: timestampValidation.error };
219
+ }
220
+ // 2. Get authority from host header
221
+ // Try multiple ways to get the host
222
+ let authority = req.header('host') || req.header(':authority') || '';
223
+ // Fallback: try to extract from URL if available
224
+ if (!authority && 'url' in req && typeof req.url === 'string') {
225
+ try {
226
+ authority = new URL(req.url).hostname;
227
+ }
228
+ catch {
229
+ // ignore URL parse errors
230
+ }
231
+ }
232
+ // Get path from req - Hono Request has .path property
233
+ let path;
234
+ if ('path' in req && typeof req.path === 'string') {
235
+ path = req.path;
236
+ }
237
+ else if ('url' in req) {
238
+ path = new URL(req.url).pathname;
239
+ }
240
+ else {
241
+ path = '/';
242
+ }
243
+ // 3. Build signature base
244
+ const signatureBase = buildEdgeSignatureBase(authority, path, parsed);
245
+ // 4. Verify cryptographic signature
246
+ const isValid = await verifyCryptoSignature(signatureBase, signature, publicKey, algorithm, parsed.label);
247
+ if (!isValid) {
248
+ return { verified: false, error: 'Signature verification failed' };
249
+ }
250
+ return {
251
+ verified: true,
252
+ tag: parsed.tag,
253
+ agentKeyId: parsed.keyId,
254
+ algorithm: parsed.algorithm,
255
+ nonce: parsed.nonce,
256
+ timestamp: parsed.created,
257
+ };
258
+ }
259
+ catch (error) {
260
+ return {
261
+ verified: false,
262
+ error: `Verification error: ${error instanceof Error ? error.message : 'Unknown error'}`
263
+ };
264
+ }
265
+ }
266
+ /**
267
+ * Build signature base string according to RFC 9421 TAP format
268
+ */
269
+ export function buildEdgeSignatureBase(authority, path, parsed) {
270
+ const lines = [];
271
+ // Add component lines (values are bare, no quotes)
272
+ for (const component of parsed.components) {
273
+ if (component === '@authority') {
274
+ lines.push(`"@authority": ${authority}`);
275
+ }
276
+ else if (component === '@path') {
277
+ lines.push(`"@path": ${path}`);
278
+ }
279
+ else if (component === '@method') {
280
+ lines.push(`"@method": GET`); // Edge typically handles GET requests
281
+ }
282
+ // Note: Other headers would need to be passed in if components include them
283
+ }
284
+ // Build @signature-params line with ALL fields
285
+ const componentsList = parsed.components.map(c => `"${c}"`).join(' ');
286
+ let paramsLine = `"@signature-params": ${parsed.label}=(${componentsList});created=${parsed.created};keyid="${parsed.keyId}";alg="${parsed.algorithm}"`;
287
+ if (parsed.expires !== undefined) {
288
+ paramsLine += `;expires=${parsed.expires}`;
289
+ }
290
+ if (parsed.nonce) {
291
+ paramsLine += `;nonce="${parsed.nonce}"`;
292
+ }
293
+ if (parsed.tag) {
294
+ paramsLine += `;tag="${parsed.tag}"`;
295
+ }
296
+ lines.push(paramsLine);
297
+ return lines.join('\n');
298
+ }
299
+ /**
300
+ * Validate created/expires timestamps according to TAP spec
301
+ */
302
+ function validateEdgeTimestamps(created, expires) {
303
+ const now = Math.floor(Date.now() / 1000);
304
+ const clockSkew = 30; // 30 seconds tolerance for clock drift
305
+ // created must be in the past (with clock skew)
306
+ if (created > now + clockSkew) {
307
+ return { valid: false, error: 'Signature timestamp is in the future' };
308
+ }
309
+ // If expires is present, validate it
310
+ if (expires !== undefined) {
311
+ // expires must be in the future
312
+ if (expires < now) {
313
+ return { valid: false, error: 'Signature has expired' };
314
+ }
315
+ // expires - created must be <= 480 seconds (8 minutes per TAP spec)
316
+ const window = expires - created;
317
+ if (window > 480) {
318
+ return { valid: false, error: 'Signature validity window exceeds 8 minutes' };
319
+ }
320
+ }
321
+ else {
322
+ // No expires - fall back to 5-minute tolerance on created (backward compat)
323
+ const age = now - created;
324
+ if (age > 300) {
325
+ return { valid: false, error: 'Signature timestamp too old' };
326
+ }
327
+ if (age < -clockSkew) {
328
+ return { valid: false, error: 'Signature timestamp too new' };
329
+ }
330
+ }
331
+ return { valid: true };
332
+ }
333
+ /**
334
+ * Verify cryptographic signature using Web Crypto API
335
+ */
336
+ async function verifyCryptoSignature(signatureBase, signature, publicKeyPem, algorithm, label) {
337
+ try {
338
+ // Extract signature bytes using the correct label
339
+ const sigPattern = new RegExp(`${label}=:([^:]+):`);
340
+ const sigMatch = signature.match(sigPattern);
341
+ if (!sigMatch)
342
+ return false;
343
+ const signatureBytes = Uint8Array.from(atob(sigMatch[1]), c => c.charCodeAt(0));
344
+ // Import public key
345
+ const keyData = importPublicKey(publicKeyPem, algorithm);
346
+ const cryptoKey = await crypto.subtle.importKey('spki', keyData, getImportParams(algorithm), false, ['verify']);
347
+ // Verify signature
348
+ const encoder = new TextEncoder();
349
+ const data = encoder.encode(signatureBase);
350
+ return await crypto.subtle.verify(getVerifyParams(algorithm), cryptoKey, signatureBytes, data);
351
+ }
352
+ catch (error) {
353
+ console.error('Crypto signature verification error:', error);
354
+ return false;
355
+ }
356
+ }
357
+ /**
358
+ * Import public key - handles PEM SPKI and raw Ed25519 formats
359
+ */
360
+ function importPublicKey(key, algorithm) {
361
+ // Check if it's a raw Ed25519 key (32 bytes base64)
362
+ if (algorithm.toLowerCase().includes('ed25519') || algorithm === 'Ed25519') {
363
+ if (isRawEd25519Key(key)) {
364
+ return rawEd25519ToSPKI(key);
365
+ }
366
+ }
367
+ // Otherwise parse as PEM
368
+ return pemToArrayBuffer(key);
369
+ }
370
+ /**
371
+ * Detect raw Ed25519 public key (32 bytes = 43-44 base64 chars)
372
+ */
373
+ function isRawEd25519Key(key) {
374
+ const stripped = key.replace(/[\s\n\r-]/g, '').replace(/BEGIN.*?END[^-]*-*/g, '');
375
+ try {
376
+ const decoded = atob(stripped.replace(/-/g, '+').replace(/_/g, '/'));
377
+ return decoded.length === 32;
378
+ }
379
+ catch {
380
+ return false;
381
+ }
382
+ }
383
+ /**
384
+ * Convert raw 32-byte Ed25519 key to SPKI format
385
+ */
386
+ function rawEd25519ToSPKI(rawKey) {
387
+ const rawBytes = Uint8Array.from(atob(rawKey), c => c.charCodeAt(0));
388
+ if (rawBytes.length !== 32) {
389
+ throw new Error('Invalid Ed25519 key length');
390
+ }
391
+ // SPKI header for Ed25519 (12 bytes)
392
+ const spkiHeader = new Uint8Array([
393
+ 0x30, 0x2a, // SEQUENCE (42 bytes)
394
+ 0x30, 0x05, // SEQUENCE (5 bytes) - algorithm
395
+ 0x06, 0x03, 0x2b, 0x65, 0x70, // OID 1.3.101.112 (Ed25519)
396
+ 0x03, 0x21, 0x00 // BIT STRING (33 bytes, 0 unused bits)
397
+ ]);
398
+ const spki = new Uint8Array(spkiHeader.length + rawBytes.length);
399
+ spki.set(spkiHeader, 0);
400
+ spki.set(rawBytes, spkiHeader.length);
401
+ return spki.buffer;
402
+ }
403
+ /**
404
+ * Convert PEM public key to ArrayBuffer
405
+ */
406
+ function pemToArrayBuffer(pem) {
407
+ const base64 = pem
408
+ .replace(/-----BEGIN PUBLIC KEY-----/, '')
409
+ .replace(/-----END PUBLIC KEY-----/, '')
410
+ .replace(/\s/g, '');
411
+ const binary = atob(base64);
412
+ const bytes = new Uint8Array(binary.length);
413
+ for (let i = 0; i < binary.length; i++) {
414
+ bytes[i] = binary.charCodeAt(i);
415
+ }
416
+ return bytes.buffer;
417
+ }
418
+ /**
419
+ * Get Web Crypto API algorithm parameters for key import
420
+ */
421
+ function getImportParams(algorithm) {
422
+ const alg = algorithm.toLowerCase();
423
+ if (alg.includes('ed25519')) {
424
+ return { name: 'Ed25519' };
425
+ }
426
+ switch (algorithm) {
427
+ case 'ecdsa-p256-sha256':
428
+ return { name: 'ECDSA', namedCurve: 'P-256' };
429
+ case 'rsa-pss-sha256':
430
+ return { name: 'RSA-PSS', hash: 'SHA-256' };
431
+ default:
432
+ throw new Error(`Unsupported algorithm: ${algorithm}`);
433
+ }
434
+ }
435
+ /**
436
+ * Get Web Crypto API algorithm parameters for signature verification
437
+ */
438
+ function getVerifyParams(algorithm) {
439
+ const alg = algorithm.toLowerCase();
440
+ if (alg.includes('ed25519')) {
441
+ return { name: 'Ed25519' };
442
+ }
443
+ switch (algorithm) {
444
+ case 'ecdsa-p256-sha256':
445
+ return { name: 'ECDSA', hash: 'SHA-256' };
446
+ case 'rsa-pss-sha256':
447
+ return { name: 'RSA-PSS', saltLength: 32 };
448
+ default:
449
+ throw new Error(`Unsupported algorithm: ${algorithm}`);
450
+ }
451
+ }
452
+ // ============ CONVENIENCE PRESETS ============
453
+ /**
454
+ * Strict mode: require TAP on all requests, block failures
455
+ */
456
+ export const tapEdgeStrict = (jwksUrls) => createTAPEdgeMiddleware({
457
+ jwksUrls,
458
+ allowUnverified: false,
459
+ requireTag: true,
460
+ blockOnFailure: true
461
+ });
462
+ /**
463
+ * Flexible mode: verify if present, pass through if not
464
+ */
465
+ export const tapEdgeFlexible = (jwksUrls) => createTAPEdgeMiddleware({
466
+ jwksUrls,
467
+ allowUnverified: true,
468
+ requireTag: false,
469
+ blockOnFailure: false
470
+ });
471
+ /**
472
+ * Development mode: log only, never block
473
+ */
474
+ export const tapEdgeDev = () => createTAPEdgeMiddleware({
475
+ allowUnverified: true,
476
+ blockOnFailure: false,
477
+ onVerified: (r) => console.log('[TAP] Verified:', r),
478
+ onFailed: (r) => console.log('[TAP] Failed:', r),
479
+ });
480
+ // ============ EXPORTS ============
481
+ export default {
482
+ createTAPEdgeMiddleware,
483
+ tapEdgeStrict,
484
+ tapEdgeFlexible,
485
+ tapEdgeDev,
486
+ TAP_EDGE_HEADERS,
487
+ };
@@ -0,0 +1,89 @@
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
+ export interface FederatedKeySource {
7
+ url: string;
8
+ name: string;
9
+ trustLevel: 'high' | 'medium' | 'low';
10
+ refreshInterval: number;
11
+ enabled: boolean;
12
+ }
13
+ export interface FederatedKey {
14
+ kid: string;
15
+ kty: string;
16
+ alg: string;
17
+ publicKeyPem: string;
18
+ source: string;
19
+ sourceUrl: string;
20
+ trustLevel: 'high' | 'medium' | 'low';
21
+ fetchedAt: number;
22
+ expiresAt: number;
23
+ x5c?: string[];
24
+ }
25
+ export interface FederationConfig {
26
+ sources: FederatedKeySource[];
27
+ kvNamespace?: KVNamespace;
28
+ defaultRefreshInterval?: number;
29
+ maxCacheAge?: number;
30
+ }
31
+ export interface KeyResolutionResult {
32
+ found: boolean;
33
+ key?: FederatedKey;
34
+ error?: string;
35
+ }
36
+ interface KVNamespace {
37
+ get(key: string, type?: string): Promise<string | null>;
38
+ put(key: string, value: string, options?: {
39
+ expirationTtl?: number;
40
+ }): Promise<void>;
41
+ }
42
+ export declare const WELL_KNOWN_SOURCES: FederatedKeySource[];
43
+ /**
44
+ * Fetch JWKS from a URL
45
+ * @throws Error if fetch fails or response is invalid
46
+ */
47
+ export declare function fetchJWKS(url: string): Promise<{
48
+ keys: any[];
49
+ }>;
50
+ /**
51
+ * Convert a JWK from an external source to FederatedKey format
52
+ */
53
+ export declare function jwkToFederatedKey(jwk: any, source: FederatedKeySource): Promise<FederatedKey>;
54
+ /**
55
+ * Resolve Web Crypto import parameters based on JWK type
56
+ */
57
+ export declare function resolveImportParams(jwk: any): any;
58
+ /**
59
+ * Infer algorithm from JWK if not specified
60
+ */
61
+ export declare function inferAlgorithm(jwk: any): string;
62
+ export interface FederationResolver {
63
+ resolveKey(kid: string): Promise<KeyResolutionResult>;
64
+ refreshAll(): Promise<{
65
+ refreshed: number;
66
+ errors: string[];
67
+ }>;
68
+ getCachedKeys(): FederatedKey[];
69
+ clearCache(): void;
70
+ }
71
+ /**
72
+ * Create a federation resolver that fetches and caches keys from external sources
73
+ */
74
+ export declare function createFederationResolver(config: FederationConfig): FederationResolver;
75
+ /**
76
+ * Create a Visa-specific federation resolver
77
+ */
78
+ export declare function createVisaFederationResolver(kvNamespace?: KVNamespace): FederationResolver;
79
+ declare const _default: {
80
+ fetchJWKS: typeof fetchJWKS;
81
+ jwkToFederatedKey: typeof jwkToFederatedKey;
82
+ createFederationResolver: typeof createFederationResolver;
83
+ createVisaFederationResolver: typeof createVisaFederationResolver;
84
+ resolveImportParams: typeof resolveImportParams;
85
+ inferAlgorithm: typeof inferAlgorithm;
86
+ WELL_KNOWN_SOURCES: FederatedKeySource[];
87
+ };
88
+ export default _default;
89
+ //# sourceMappingURL=tap-federation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tap-federation.d.ts","sourceRoot":"","sources":["../src/tap-federation.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACtC,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,YAAY,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAGD,UAAU,WAAW;IACnB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtF;AAID,eAAO,MAAM,kBAAkB,EAAE,kBAAkB,EASlD,CAAC;AAIF;;;GAGG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,CAiBrE;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,YAAY,CAAC,CAuBvB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,CAcjD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAK/C;AAaD,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACtD,UAAU,IAAI,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IAC/D,aAAa,IAAI,YAAY,EAAE,CAAC;IAChC,UAAU,IAAI,IAAI,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,gBAAgB,GAAG,kBAAkB,CAiIrF;AAID;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,WAAW,CAAC,EAAE,WAAW,GAAG,kBAAkB,CAO1F;;;;;;;;;;AAID,wBAQE"}