@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
package/dist/tap-verify.js
CHANGED
|
@@ -4,13 +4,20 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Integrates with existing BOTCHA verification middleware to provide
|
|
6
6
|
* enterprise-grade cryptographic agent authentication
|
|
7
|
+
*
|
|
8
|
+
* FEATURES:
|
|
9
|
+
* - Ed25519, ECDSA P-256, RSA-PSS signature verification
|
|
10
|
+
* - Full RFC 9421 compliance (sig1/sig2 labels, expires, nonce, tag)
|
|
11
|
+
* - Nonce replay protection via KV
|
|
12
|
+
* - TAP timestamp validation (created, expires, 8-minute max window)
|
|
13
|
+
* - Backward compatible with existing BOTCHA agents
|
|
7
14
|
*/
|
|
8
15
|
import { TAP_VALID_ACTIONS } from './tap-agents.js';
|
|
9
16
|
// ============ HTTP MESSAGE SIGNATURES (RFC 9421) ============
|
|
10
17
|
/**
|
|
11
18
|
* Verify HTTP Message Signature according to RFC 9421
|
|
12
19
|
*/
|
|
13
|
-
export async function verifyHTTPMessageSignature(request, publicKey, algorithm) {
|
|
20
|
+
export async function verifyHTTPMessageSignature(request, publicKey, algorithm, nonces) {
|
|
14
21
|
try {
|
|
15
22
|
const { headers } = request;
|
|
16
23
|
const signature = headers['signature'];
|
|
@@ -23,19 +30,31 @@ export async function verifyHTTPMessageSignature(request, publicKey, algorithm)
|
|
|
23
30
|
if (!parsed) {
|
|
24
31
|
return { valid: false, error: 'Invalid signature-input format' };
|
|
25
32
|
}
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
// Validate timestamps
|
|
34
|
+
const timestampValidation = validateTimestamps(parsed.created, parsed.expires);
|
|
35
|
+
if (!timestampValidation.valid) {
|
|
36
|
+
return { valid: false, error: timestampValidation.error };
|
|
37
|
+
}
|
|
38
|
+
// Check nonce replay (if nonce provided and KV available)
|
|
39
|
+
if (parsed.nonce && nonces) {
|
|
40
|
+
const nonceCheck = await checkAndStoreNonce(nonces, parsed.nonce);
|
|
41
|
+
if (nonceCheck.replay) {
|
|
42
|
+
return { valid: false, error: 'Nonce replay detected' };
|
|
32
43
|
}
|
|
33
44
|
}
|
|
34
45
|
// Build signature base
|
|
35
|
-
const signatureBase = buildSignatureBase(request.method, request.path, headers, parsed
|
|
46
|
+
const signatureBase = buildSignatureBase(request.method, request.path, headers, parsed);
|
|
36
47
|
// Verify signature
|
|
37
|
-
const isValid = await verifyCryptoSignature(signatureBase, signature, publicKey, algorithm);
|
|
38
|
-
return {
|
|
48
|
+
const isValid = await verifyCryptoSignature(signatureBase, signature, publicKey, algorithm, parsed.label);
|
|
49
|
+
return {
|
|
50
|
+
valid: isValid,
|
|
51
|
+
error: isValid ? undefined : 'Signature verification failed',
|
|
52
|
+
metadata: {
|
|
53
|
+
nonce: parsed.nonce,
|
|
54
|
+
tag: parsed.tag,
|
|
55
|
+
key_id: parsed.keyId
|
|
56
|
+
}
|
|
57
|
+
};
|
|
39
58
|
}
|
|
40
59
|
catch (error) {
|
|
41
60
|
return { valid: false, error: `Verification error: ${error instanceof Error ? error.message : 'Unknown error'}` };
|
|
@@ -43,25 +62,35 @@ export async function verifyHTTPMessageSignature(request, publicKey, algorithm)
|
|
|
43
62
|
}
|
|
44
63
|
/**
|
|
45
64
|
* Parse signature-input header according to RFC 9421
|
|
65
|
+
* Supports BOTH sig1 (BOTCHA) and sig2 (Visa TAP) labels
|
|
46
66
|
*/
|
|
47
67
|
function parseSignatureInput(input) {
|
|
48
68
|
try {
|
|
49
|
-
//
|
|
50
|
-
const sigMatch = input.match(/
|
|
69
|
+
// Match sig1 OR sig2
|
|
70
|
+
const sigMatch = input.match(/(sig[12])=\(([^)]+)\)/);
|
|
51
71
|
if (!sigMatch)
|
|
52
72
|
return null;
|
|
53
|
-
const
|
|
73
|
+
const label = sigMatch[1];
|
|
74
|
+
const components = sigMatch[2]
|
|
54
75
|
.split(' ')
|
|
55
76
|
.map(h => h.replace(/"/g, ''));
|
|
56
|
-
|
|
77
|
+
// Extract all params (keyid/keyId, alg, created, expires, nonce, tag)
|
|
78
|
+
const keyIdMatch = input.match(/keyid="([^"]+)"/i);
|
|
57
79
|
const algMatch = input.match(/alg="([^"]+)"/);
|
|
58
80
|
const createdMatch = input.match(/created=(\d+)/);
|
|
81
|
+
const expiresMatch = input.match(/expires=(\d+)/);
|
|
82
|
+
const nonceMatch = input.match(/nonce="([^"]+)"/);
|
|
83
|
+
const tagMatch = input.match(/tag="([^"]+)"/);
|
|
59
84
|
if (!keyIdMatch || !algMatch || !createdMatch)
|
|
60
85
|
return null;
|
|
61
86
|
return {
|
|
87
|
+
label,
|
|
62
88
|
keyId: keyIdMatch[1],
|
|
63
89
|
algorithm: algMatch[1],
|
|
64
90
|
created: parseInt(createdMatch[1]),
|
|
91
|
+
expires: expiresMatch ? parseInt(expiresMatch[1]) : undefined,
|
|
92
|
+
nonce: nonceMatch ? nonceMatch[1] : undefined,
|
|
93
|
+
tag: tagMatch ? tagMatch[1] : undefined,
|
|
65
94
|
components
|
|
66
95
|
};
|
|
67
96
|
}
|
|
@@ -70,11 +99,51 @@ function parseSignatureInput(input) {
|
|
|
70
99
|
}
|
|
71
100
|
}
|
|
72
101
|
/**
|
|
73
|
-
*
|
|
102
|
+
* Validate created/expires timestamps according to TAP spec
|
|
74
103
|
*/
|
|
75
|
-
function
|
|
104
|
+
function validateTimestamps(created, expires) {
|
|
105
|
+
const now = Math.floor(Date.now() / 1000);
|
|
106
|
+
const clockSkew = 30; // 30 seconds tolerance for clock drift
|
|
107
|
+
// created must be in the past (with clock skew)
|
|
108
|
+
if (created > now + clockSkew) {
|
|
109
|
+
return { valid: false, error: 'Signature timestamp is in the future' };
|
|
110
|
+
}
|
|
111
|
+
// If expires is present, validate it
|
|
112
|
+
if (expires !== undefined) {
|
|
113
|
+
// expires must be in the future
|
|
114
|
+
if (expires < now) {
|
|
115
|
+
return { valid: false, error: 'Signature has expired' };
|
|
116
|
+
}
|
|
117
|
+
// expires - created must be <= 480 seconds (8 minutes per TAP spec)
|
|
118
|
+
const window = expires - created;
|
|
119
|
+
if (window > 480) {
|
|
120
|
+
return { valid: false, error: 'Signature validity window exceeds 8 minutes' };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// No expires - fall back to 5-minute tolerance on created (backward compat)
|
|
125
|
+
const age = now - created;
|
|
126
|
+
if (age > 300) {
|
|
127
|
+
return { valid: false, error: 'Signature timestamp too old or too new' };
|
|
128
|
+
}
|
|
129
|
+
if (age < -clockSkew) {
|
|
130
|
+
return { valid: false, error: 'Signature timestamp too old or too new' };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { valid: true };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Build signature base string according to RFC 9421 TAP format
|
|
137
|
+
*
|
|
138
|
+
* Format:
|
|
139
|
+
* "@authority": example.com
|
|
140
|
+
* "@path": /example-product
|
|
141
|
+
* "@signature-params": sig2=("@authority" "@path");created=1735689600;keyid="poqk...";alg="Ed25519";expires=1735693200;nonce="e8N7...";tag="agent-browser-auth"
|
|
142
|
+
*/
|
|
143
|
+
function buildSignatureBase(method, path, headers, parsed) {
|
|
76
144
|
const lines = [];
|
|
77
|
-
|
|
145
|
+
// Add component lines (values are bare, no quotes)
|
|
146
|
+
for (const component of parsed.components) {
|
|
78
147
|
if (component === '@method') {
|
|
79
148
|
lines.push(`"@method": ${method.toUpperCase()}`);
|
|
80
149
|
}
|
|
@@ -82,7 +151,8 @@ function buildSignatureBase(method, path, headers, components, created, keyId, a
|
|
|
82
151
|
lines.push(`"@path": ${path}`);
|
|
83
152
|
}
|
|
84
153
|
else if (component === '@authority') {
|
|
85
|
-
|
|
154
|
+
const authority = headers['host'] || headers[':authority'] || '';
|
|
155
|
+
lines.push(`"@authority": ${authority}`);
|
|
86
156
|
}
|
|
87
157
|
else {
|
|
88
158
|
const value = headers[component];
|
|
@@ -91,23 +161,34 @@ function buildSignatureBase(method, path, headers, components, created, keyId, a
|
|
|
91
161
|
}
|
|
92
162
|
}
|
|
93
163
|
}
|
|
94
|
-
//
|
|
95
|
-
const componentsList = components.map(c => `"${c}"`).join(' ');
|
|
96
|
-
|
|
164
|
+
// Build @signature-params line with ALL fields
|
|
165
|
+
const componentsList = parsed.components.map(c => `"${c}"`).join(' ');
|
|
166
|
+
let paramsLine = `"@signature-params": ${parsed.label}=(${componentsList});created=${parsed.created};keyid="${parsed.keyId}";alg="${parsed.algorithm}"`;
|
|
167
|
+
if (parsed.expires !== undefined) {
|
|
168
|
+
paramsLine += `;expires=${parsed.expires}`;
|
|
169
|
+
}
|
|
170
|
+
if (parsed.nonce) {
|
|
171
|
+
paramsLine += `;nonce="${parsed.nonce}"`;
|
|
172
|
+
}
|
|
173
|
+
if (parsed.tag) {
|
|
174
|
+
paramsLine += `;tag="${parsed.tag}"`;
|
|
175
|
+
}
|
|
176
|
+
lines.push(paramsLine);
|
|
97
177
|
return lines.join('\n');
|
|
98
178
|
}
|
|
99
179
|
/**
|
|
100
180
|
* Verify cryptographic signature using Web Crypto API
|
|
101
181
|
*/
|
|
102
|
-
async function verifyCryptoSignature(signatureBase, signature, publicKeyPem, algorithm) {
|
|
182
|
+
async function verifyCryptoSignature(signatureBase, signature, publicKeyPem, algorithm, label) {
|
|
103
183
|
try {
|
|
104
|
-
// Extract signature bytes
|
|
105
|
-
const
|
|
184
|
+
// Extract signature bytes using the correct label
|
|
185
|
+
const sigPattern = new RegExp(`${label}=:([^:]+):`);
|
|
186
|
+
const sigMatch = signature.match(sigPattern);
|
|
106
187
|
if (!sigMatch)
|
|
107
188
|
return false;
|
|
108
189
|
const signatureBytes = Uint8Array.from(atob(sigMatch[1]), c => c.charCodeAt(0));
|
|
109
|
-
// Import public key
|
|
110
|
-
const keyData =
|
|
190
|
+
// Import public key (handles both PEM and raw Ed25519)
|
|
191
|
+
const keyData = importPublicKey(publicKeyPem, algorithm);
|
|
111
192
|
const cryptoKey = await crypto.subtle.importKey('spki', keyData, getImportParams(algorithm), false, ['verify']);
|
|
112
193
|
// Verify signature
|
|
113
194
|
const encoder = new TextEncoder();
|
|
@@ -119,6 +200,53 @@ async function verifyCryptoSignature(signatureBase, signature, publicKeyPem, alg
|
|
|
119
200
|
return false;
|
|
120
201
|
}
|
|
121
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Import public key - handles PEM SPKI and raw Ed25519 formats
|
|
205
|
+
*/
|
|
206
|
+
function importPublicKey(key, algorithm) {
|
|
207
|
+
// Check if it's a raw Ed25519 key (32 bytes base64)
|
|
208
|
+
if (algorithm.toLowerCase().includes('ed25519') || algorithm === 'Ed25519') {
|
|
209
|
+
if (isRawEd25519Key(key)) {
|
|
210
|
+
return rawEd25519ToSPKI(key);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Otherwise parse as PEM
|
|
214
|
+
return pemToArrayBuffer(key);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Detect raw Ed25519 public key (32 bytes = 43-44 base64 chars)
|
|
218
|
+
*/
|
|
219
|
+
function isRawEd25519Key(key) {
|
|
220
|
+
const stripped = key.replace(/[\s\n\r-]/g, '').replace(/BEGIN.*?END[^-]*-*/g, '');
|
|
221
|
+
try {
|
|
222
|
+
const decoded = atob(stripped.replace(/-/g, '+').replace(/_/g, '/'));
|
|
223
|
+
return decoded.length === 32;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Convert raw 32-byte Ed25519 key to SPKI format
|
|
231
|
+
* SPKI format: ASN.1 header + 32-byte public key
|
|
232
|
+
*/
|
|
233
|
+
function rawEd25519ToSPKI(rawKey) {
|
|
234
|
+
const rawBytes = Uint8Array.from(atob(rawKey), c => c.charCodeAt(0));
|
|
235
|
+
if (rawBytes.length !== 32) {
|
|
236
|
+
throw new Error('Invalid Ed25519 key length');
|
|
237
|
+
}
|
|
238
|
+
// SPKI header for Ed25519 (12 bytes)
|
|
239
|
+
const spkiHeader = new Uint8Array([
|
|
240
|
+
0x30, 0x2a, // SEQUENCE (42 bytes)
|
|
241
|
+
0x30, 0x05, // SEQUENCE (5 bytes) - algorithm
|
|
242
|
+
0x06, 0x03, 0x2b, 0x65, 0x70, // OID 1.3.101.112 (Ed25519)
|
|
243
|
+
0x03, 0x21, 0x00 // BIT STRING (33 bytes, 0 unused bits)
|
|
244
|
+
]);
|
|
245
|
+
const spki = new Uint8Array(spkiHeader.length + rawBytes.length);
|
|
246
|
+
spki.set(spkiHeader, 0);
|
|
247
|
+
spki.set(rawBytes, spkiHeader.length);
|
|
248
|
+
return spki.buffer;
|
|
249
|
+
}
|
|
122
250
|
/**
|
|
123
251
|
* Convert PEM public key to ArrayBuffer
|
|
124
252
|
*/
|
|
@@ -138,6 +266,10 @@ function pemToArrayBuffer(pem) {
|
|
|
138
266
|
* Get Web Crypto API algorithm parameters for key import
|
|
139
267
|
*/
|
|
140
268
|
function getImportParams(algorithm) {
|
|
269
|
+
const alg = algorithm.toLowerCase();
|
|
270
|
+
if (alg.includes('ed25519')) {
|
|
271
|
+
return { name: 'Ed25519' }; // No hash or curve needed
|
|
272
|
+
}
|
|
141
273
|
switch (algorithm) {
|
|
142
274
|
case 'ecdsa-p256-sha256':
|
|
143
275
|
return { name: 'ECDSA', namedCurve: 'P-256' };
|
|
@@ -151,6 +283,10 @@ function getImportParams(algorithm) {
|
|
|
151
283
|
* Get Web Crypto API algorithm parameters for signature verification
|
|
152
284
|
*/
|
|
153
285
|
function getVerifyParams(algorithm) {
|
|
286
|
+
const alg = algorithm.toLowerCase();
|
|
287
|
+
if (alg.includes('ed25519')) {
|
|
288
|
+
return { name: 'Ed25519' }; // No hash needed
|
|
289
|
+
}
|
|
154
290
|
switch (algorithm) {
|
|
155
291
|
case 'ecdsa-p256-sha256':
|
|
156
292
|
return { name: 'ECDSA', hash: 'SHA-256' };
|
|
@@ -160,6 +296,36 @@ function getVerifyParams(algorithm) {
|
|
|
160
296
|
throw new Error(`Unsupported algorithm: ${algorithm}`);
|
|
161
297
|
}
|
|
162
298
|
}
|
|
299
|
+
// ============ NONCE REPLAY PROTECTION ============
|
|
300
|
+
/**
|
|
301
|
+
* Check if nonce was already used and store it if new
|
|
302
|
+
* Returns { replay: true } if nonce was seen before
|
|
303
|
+
*/
|
|
304
|
+
export async function checkAndStoreNonce(nonces, nonce) {
|
|
305
|
+
if (!nonces || !nonce)
|
|
306
|
+
return { replay: false };
|
|
307
|
+
try {
|
|
308
|
+
// Hash nonce for fixed-length KV key
|
|
309
|
+
const encoder = new TextEncoder();
|
|
310
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(nonce));
|
|
311
|
+
const hashHex = Array.from(new Uint8Array(hashBuffer))
|
|
312
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
313
|
+
.join('');
|
|
314
|
+
const key = `nonce:${hashHex}`;
|
|
315
|
+
// Check if already seen
|
|
316
|
+
const existing = await nonces.get(key);
|
|
317
|
+
if (existing)
|
|
318
|
+
return { replay: true };
|
|
319
|
+
// Store with 8-minute TTL (480 seconds per TAP spec)
|
|
320
|
+
await nonces.put(key, '1', { expirationTtl: 480 });
|
|
321
|
+
return { replay: false };
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
console.error('Nonce check error:', error);
|
|
325
|
+
// Fail-open on KV errors (don't block legitimate requests)
|
|
326
|
+
return { replay: false };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
163
329
|
// ============ TAP INTENT VALIDATION ============
|
|
164
330
|
/**
|
|
165
331
|
* Parse and validate TAP intent from headers
|
|
@@ -190,6 +356,7 @@ export function parseTAPIntent(intentString) {
|
|
|
190
356
|
// ============ TAP HEADER EXTRACTION ============
|
|
191
357
|
/**
|
|
192
358
|
* Extract TAP-specific headers from request
|
|
359
|
+
* Supports BOTH standard TAP (sig2 + tag) and BOTCHA extended (x-tap-* headers)
|
|
193
360
|
*/
|
|
194
361
|
export function extractTAPHeaders(headers) {
|
|
195
362
|
const tapHeaders = {
|
|
@@ -200,11 +367,18 @@ export function extractTAPHeaders(headers) {
|
|
|
200
367
|
'signature': headers['signature'],
|
|
201
368
|
'signature-input': headers['signature-input']
|
|
202
369
|
};
|
|
203
|
-
|
|
370
|
+
// Check for standard TAP (sig2 + agent tag)
|
|
371
|
+
const signatureInput = headers['signature-input'] || '';
|
|
372
|
+
const hasAgentTag = /tag="agent-(browser|payer)-auth"/.test(signatureInput);
|
|
373
|
+
const hasSig2 = /sig2=\(/.test(signatureInput);
|
|
374
|
+
const isTAPStandard = hasAgentTag && hasSig2;
|
|
375
|
+
// BOTCHA extended: requires x-tap-agent-id + x-tap-intent + signature
|
|
376
|
+
const hasBOTCHAExtended = Boolean(tapHeaders['x-tap-agent-id'] &&
|
|
204
377
|
tapHeaders['x-tap-intent'] &&
|
|
205
378
|
tapHeaders['signature'] &&
|
|
206
379
|
tapHeaders['signature-input']);
|
|
207
|
-
|
|
380
|
+
const hasTAPHeaders = isTAPStandard || hasBOTCHAExtended;
|
|
381
|
+
return { hasTAPHeaders, tapHeaders, isTAPStandard };
|
|
208
382
|
}
|
|
209
383
|
// ============ VERIFICATION MODES ============
|
|
210
384
|
/**
|
|
@@ -226,6 +400,14 @@ export function getVerificationMode(headers) {
|
|
|
226
400
|
}
|
|
227
401
|
return { mode, hasTAPHeaders, hasChallenge };
|
|
228
402
|
}
|
|
403
|
+
// ============ TAG MAPPING ============
|
|
404
|
+
/**
|
|
405
|
+
* Map TAP action to appropriate tag
|
|
406
|
+
*/
|
|
407
|
+
export function actionToTag(action) {
|
|
408
|
+
const payerActions = ['purchase'];
|
|
409
|
+
return payerActions.includes(action) ? 'agent-payer-auth' : 'agent-browser-auth';
|
|
410
|
+
}
|
|
229
411
|
// ============ CHALLENGE RESPONSE BUILDERS ============
|
|
230
412
|
/**
|
|
231
413
|
* Build appropriate challenge response for TAP verification failure
|
|
@@ -262,7 +444,8 @@ export function buildTAPChallengeResponse(verificationResult, challengeData) {
|
|
|
262
444
|
'signature',
|
|
263
445
|
'signature-input'
|
|
264
446
|
],
|
|
265
|
-
supported_algorithms: ['ecdsa-p256-sha256', 'rsa-pss-sha256']
|
|
447
|
+
supported_algorithms: ['ed25519', 'Ed25519', 'ecdsa-p256-sha256', 'rsa-pss-sha256'],
|
|
448
|
+
jwks_url: 'https://botcha.ai/.well-known/jwks'
|
|
266
449
|
};
|
|
267
450
|
return response;
|
|
268
451
|
}
|
|
@@ -271,5 +454,7 @@ export default {
|
|
|
271
454
|
parseTAPIntent,
|
|
272
455
|
extractTAPHeaders,
|
|
273
456
|
getVerificationMode,
|
|
274
|
-
buildTAPChallengeResponse
|
|
457
|
+
buildTAPChallengeResponse,
|
|
458
|
+
checkAndStoreNonce,
|
|
459
|
+
actionToTag
|
|
275
460
|
};
|