@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,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TAP Consumer Recognition — Agentic Consumer Recognition Object
|
|
3
|
+
* Visa TAP Layer 2: Consumer identity verification linked to message signature
|
|
4
|
+
*
|
|
5
|
+
* Implements nonce-linked signature chains where:
|
|
6
|
+
* 1. HTTP message signature (Layer 1) contains a nonce
|
|
7
|
+
* 2. Consumer object contains the same nonce + consumer data
|
|
8
|
+
* 3. Consumer object is signed with same key
|
|
9
|
+
*
|
|
10
|
+
* This proves the agent had authorization from the consumer at signature time.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Agentic Consumer Recognition Object
|
|
14
|
+
* JSON structure in request body proving consumer authorization
|
|
15
|
+
*/
|
|
16
|
+
export interface AgenticConsumer {
|
|
17
|
+
nonce: string;
|
|
18
|
+
idToken?: IDTokenJWT | string;
|
|
19
|
+
contextualData?: ContextualData;
|
|
20
|
+
kid: string;
|
|
21
|
+
alg: string;
|
|
22
|
+
signature: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parsed ID Token (OIDC-compatible JWT)
|
|
26
|
+
*/
|
|
27
|
+
export interface IDTokenJWT {
|
|
28
|
+
header: {
|
|
29
|
+
typ?: string;
|
|
30
|
+
alg: string;
|
|
31
|
+
kid?: string;
|
|
32
|
+
};
|
|
33
|
+
payload: IDTokenClaims;
|
|
34
|
+
signature: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* ID Token claims (OIDC-compatible)
|
|
38
|
+
*/
|
|
39
|
+
export interface IDTokenClaims {
|
|
40
|
+
iss: string;
|
|
41
|
+
sub: string;
|
|
42
|
+
aud: string | string[];
|
|
43
|
+
exp: number;
|
|
44
|
+
iat: number;
|
|
45
|
+
jti?: string;
|
|
46
|
+
auth_time?: number;
|
|
47
|
+
amr?: string[];
|
|
48
|
+
phone_number?: string;
|
|
49
|
+
phone_number_verified?: boolean;
|
|
50
|
+
email?: string;
|
|
51
|
+
email_verified?: boolean;
|
|
52
|
+
phone_number_mask?: string;
|
|
53
|
+
email_mask?: string;
|
|
54
|
+
[key: string]: any;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Contextual data about consumer device/location
|
|
58
|
+
*/
|
|
59
|
+
export interface ContextualData {
|
|
60
|
+
countryCode?: string;
|
|
61
|
+
zip?: string;
|
|
62
|
+
ipAddress?: string;
|
|
63
|
+
deviceData?: DeviceData;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Device fingerprint data
|
|
67
|
+
*/
|
|
68
|
+
export interface DeviceData {
|
|
69
|
+
userAgent?: string;
|
|
70
|
+
screenResolution?: string;
|
|
71
|
+
language?: string;
|
|
72
|
+
timezone?: string;
|
|
73
|
+
[key: string]: any;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Result of consumer verification
|
|
77
|
+
*/
|
|
78
|
+
export interface ConsumerVerificationResult {
|
|
79
|
+
verified: boolean;
|
|
80
|
+
nonceLinked: boolean;
|
|
81
|
+
signatureValid: boolean;
|
|
82
|
+
idTokenValid?: boolean;
|
|
83
|
+
idTokenClaims?: IDTokenClaims;
|
|
84
|
+
contextualData?: ContextualData;
|
|
85
|
+
error?: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* JWKS (JSON Web Key Set) structure
|
|
89
|
+
*/
|
|
90
|
+
export interface JWKS {
|
|
91
|
+
keys: JWK[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* JSON Web Key
|
|
95
|
+
*/
|
|
96
|
+
export interface JWK {
|
|
97
|
+
kid: string;
|
|
98
|
+
kty: string;
|
|
99
|
+
alg?: string;
|
|
100
|
+
use?: string;
|
|
101
|
+
n?: string;
|
|
102
|
+
e?: string;
|
|
103
|
+
crv?: string;
|
|
104
|
+
x?: string;
|
|
105
|
+
y?: string;
|
|
106
|
+
[key: string]: any;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Parse AgenticConsumer from request body
|
|
110
|
+
* Handles both nested { agenticConsumer: {...} } and top-level formats
|
|
111
|
+
*/
|
|
112
|
+
export declare function parseAgenticConsumer(body: any): AgenticConsumer | null;
|
|
113
|
+
/**
|
|
114
|
+
* Parse ID Token JWT string into structured format
|
|
115
|
+
* Does NOT verify signature - just parses and validates structure
|
|
116
|
+
*/
|
|
117
|
+
export declare function parseIDToken(tokenString: string): IDTokenClaims | null;
|
|
118
|
+
/**
|
|
119
|
+
* Verify ID Token signature by fetching JWKS from issuer
|
|
120
|
+
* Full cryptographic verification of the JWT
|
|
121
|
+
*/
|
|
122
|
+
export declare function verifyIDTokenSignature(tokenString: string, jwksUrl?: string): Promise<{
|
|
123
|
+
valid: boolean;
|
|
124
|
+
claims?: IDTokenClaims;
|
|
125
|
+
error?: string;
|
|
126
|
+
}>;
|
|
127
|
+
/**
|
|
128
|
+
* Build canonical signature base string for consumer object
|
|
129
|
+
* Per TAP spec: all fields in order except 'signature' itself
|
|
130
|
+
*/
|
|
131
|
+
export declare function buildConsumerSignatureBase(consumer: AgenticConsumer): string;
|
|
132
|
+
/**
|
|
133
|
+
* Verify AgenticConsumer object signature and nonce linkage
|
|
134
|
+
* Main verification function for TAP Layer 2
|
|
135
|
+
*/
|
|
136
|
+
export declare function verifyAgenticConsumer(consumer: AgenticConsumer, headerNonce: string, publicKey: string, algorithm?: string): Promise<ConsumerVerificationResult>;
|
|
137
|
+
/**
|
|
138
|
+
* Hash-match obfuscated identity with cleartext
|
|
139
|
+
* Merchants maintain mapping tables with hashed values
|
|
140
|
+
*/
|
|
141
|
+
export declare function hashMatchIdentity(obfuscated: string, cleartext: string, method?: 'sha256'): Promise<boolean>;
|
|
142
|
+
declare const _default: {
|
|
143
|
+
parseAgenticConsumer: typeof parseAgenticConsumer;
|
|
144
|
+
verifyAgenticConsumer: typeof verifyAgenticConsumer;
|
|
145
|
+
parseIDToken: typeof parseIDToken;
|
|
146
|
+
verifyIDTokenSignature: typeof verifyIDTokenSignature;
|
|
147
|
+
hashMatchIdentity: typeof hashMatchIdentity;
|
|
148
|
+
buildConsumerSignatureBase: typeof buildConsumerSignatureBase;
|
|
149
|
+
};
|
|
150
|
+
export default _default;
|
|
151
|
+
//# sourceMappingURL=tap-consumer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tap-consumer.d.ts","sourceRoot":"","sources":["../src/tap-consumer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TAP Consumer Recognition — Agentic Consumer Recognition Object
|
|
3
|
+
* Visa TAP Layer 2: Consumer identity verification linked to message signature
|
|
4
|
+
*
|
|
5
|
+
* Implements nonce-linked signature chains where:
|
|
6
|
+
* 1. HTTP message signature (Layer 1) contains a nonce
|
|
7
|
+
* 2. Consumer object contains the same nonce + consumer data
|
|
8
|
+
* 3. Consumer object is signed with same key
|
|
9
|
+
*
|
|
10
|
+
* This proves the agent had authorization from the consumer at signature time.
|
|
11
|
+
*/
|
|
12
|
+
// ============ PARSING ============
|
|
13
|
+
/**
|
|
14
|
+
* Parse AgenticConsumer from request body
|
|
15
|
+
* Handles both nested { agenticConsumer: {...} } and top-level formats
|
|
16
|
+
*/
|
|
17
|
+
export function parseAgenticConsumer(body) {
|
|
18
|
+
try {
|
|
19
|
+
// Try nested format first
|
|
20
|
+
let consumer = body?.agenticConsumer;
|
|
21
|
+
// If not nested, check if body itself is the consumer object
|
|
22
|
+
if (!consumer && body?.nonce && body?.kid && body?.alg && body?.signature) {
|
|
23
|
+
consumer = body;
|
|
24
|
+
}
|
|
25
|
+
if (!consumer) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
// Validate required fields
|
|
29
|
+
if (typeof consumer.nonce !== 'string' ||
|
|
30
|
+
typeof consumer.kid !== 'string' ||
|
|
31
|
+
typeof consumer.alg !== 'string' ||
|
|
32
|
+
typeof consumer.signature !== 'string') {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
nonce: consumer.nonce,
|
|
37
|
+
idToken: consumer.idToken,
|
|
38
|
+
contextualData: consumer.contextualData,
|
|
39
|
+
kid: consumer.kid,
|
|
40
|
+
alg: consumer.alg,
|
|
41
|
+
signature: consumer.signature
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parse ID Token JWT string into structured format
|
|
50
|
+
* Does NOT verify signature - just parses and validates structure
|
|
51
|
+
*/
|
|
52
|
+
export function parseIDToken(tokenString) {
|
|
53
|
+
try {
|
|
54
|
+
const parts = tokenString.split('.');
|
|
55
|
+
if (parts.length !== 3) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
// Base64url decode header
|
|
59
|
+
const headerJson = atob(parts[0].replace(/-/g, '+').replace(/_/g, '/'));
|
|
60
|
+
const header = JSON.parse(headerJson);
|
|
61
|
+
// Base64url decode payload
|
|
62
|
+
const payloadJson = atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'));
|
|
63
|
+
const claims = JSON.parse(payloadJson);
|
|
64
|
+
// Validate required claims
|
|
65
|
+
if (typeof claims.iss !== 'string' ||
|
|
66
|
+
typeof claims.sub !== 'string' ||
|
|
67
|
+
!claims.aud ||
|
|
68
|
+
typeof claims.exp !== 'number' ||
|
|
69
|
+
typeof claims.iat !== 'number') {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
// Check expiration
|
|
73
|
+
const now = Math.floor(Date.now() / 1000);
|
|
74
|
+
if (claims.exp <= now) {
|
|
75
|
+
return null; // Token expired
|
|
76
|
+
}
|
|
77
|
+
return claims;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Verify ID Token signature by fetching JWKS from issuer
|
|
85
|
+
* Full cryptographic verification of the JWT
|
|
86
|
+
*/
|
|
87
|
+
export async function verifyIDTokenSignature(tokenString, jwksUrl) {
|
|
88
|
+
try {
|
|
89
|
+
const parts = tokenString.split('.');
|
|
90
|
+
if (parts.length !== 3) {
|
|
91
|
+
return { valid: false, error: 'Invalid JWT format' };
|
|
92
|
+
}
|
|
93
|
+
// Parse header and payload
|
|
94
|
+
const headerJson = atob(parts[0].replace(/-/g, '+').replace(/_/g, '/'));
|
|
95
|
+
const header = JSON.parse(headerJson);
|
|
96
|
+
const payloadJson = atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'));
|
|
97
|
+
const claims = JSON.parse(payloadJson);
|
|
98
|
+
// If no JWKS URL provided, just parse without verification
|
|
99
|
+
if (!jwksUrl) {
|
|
100
|
+
// Check expiration at least
|
|
101
|
+
const now = Math.floor(Date.now() / 1000);
|
|
102
|
+
if (claims.exp && claims.exp <= now) {
|
|
103
|
+
return { valid: false, error: 'Token expired', claims };
|
|
104
|
+
}
|
|
105
|
+
return { valid: true, claims }; // Unverified but parsed
|
|
106
|
+
}
|
|
107
|
+
// Fetch JWKS
|
|
108
|
+
const jwksResponse = await fetch(jwksUrl);
|
|
109
|
+
if (!jwksResponse.ok) {
|
|
110
|
+
return { valid: false, error: 'Failed to fetch JWKS' };
|
|
111
|
+
}
|
|
112
|
+
const jwks = await jwksResponse.json();
|
|
113
|
+
// Find matching key by kid
|
|
114
|
+
const key = jwks.keys.find(k => k.kid === header.kid);
|
|
115
|
+
if (!key) {
|
|
116
|
+
return { valid: false, error: 'Key not found in JWKS' };
|
|
117
|
+
}
|
|
118
|
+
// Build signature base (header.payload)
|
|
119
|
+
const signatureBase = `${parts[0]}.${parts[1]}`;
|
|
120
|
+
const signatureBytes = base64UrlToArrayBuffer(parts[2]);
|
|
121
|
+
// Import public key from JWK
|
|
122
|
+
const cryptoKey = await jwkToCryptoKey(key, header.alg);
|
|
123
|
+
// Verify signature
|
|
124
|
+
const encoder = new TextEncoder();
|
|
125
|
+
const data = encoder.encode(signatureBase);
|
|
126
|
+
const { importParams, verifyParams } = jwkAlgToWebCrypto(header.alg);
|
|
127
|
+
const isValid = await crypto.subtle.verify(verifyParams, cryptoKey, signatureBytes, data);
|
|
128
|
+
if (!isValid) {
|
|
129
|
+
return { valid: false, error: 'Signature verification failed' };
|
|
130
|
+
}
|
|
131
|
+
// Check expiration
|
|
132
|
+
const now = Math.floor(Date.now() / 1000);
|
|
133
|
+
if (claims.exp && claims.exp <= now) {
|
|
134
|
+
return { valid: false, error: 'Token expired', claims };
|
|
135
|
+
}
|
|
136
|
+
return { valid: true, claims };
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
return {
|
|
140
|
+
valid: false,
|
|
141
|
+
error: `Verification error: ${error instanceof Error ? error.message : 'Unknown'}`
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// ============ SIGNATURE VERIFICATION ============
|
|
146
|
+
/**
|
|
147
|
+
* Build canonical signature base string for consumer object
|
|
148
|
+
* Per TAP spec: all fields in order except 'signature' itself
|
|
149
|
+
*/
|
|
150
|
+
export function buildConsumerSignatureBase(consumer) {
|
|
151
|
+
const fields = [];
|
|
152
|
+
// Add fields IN ORDER, excluding 'signature'
|
|
153
|
+
if (consumer.nonce) {
|
|
154
|
+
fields.push(`"nonce": "${consumer.nonce}"`);
|
|
155
|
+
}
|
|
156
|
+
if (consumer.idToken) {
|
|
157
|
+
const idTokenStr = typeof consumer.idToken === 'string'
|
|
158
|
+
? `"${consumer.idToken}"`
|
|
159
|
+
: JSON.stringify(consumer.idToken);
|
|
160
|
+
fields.push(`"idToken": ${idTokenStr}`);
|
|
161
|
+
}
|
|
162
|
+
if (consumer.contextualData) {
|
|
163
|
+
fields.push(`"contextualData": ${JSON.stringify(consumer.contextualData)}`);
|
|
164
|
+
}
|
|
165
|
+
if (consumer.kid) {
|
|
166
|
+
fields.push(`"kid": "${consumer.kid}"`);
|
|
167
|
+
}
|
|
168
|
+
if (consumer.alg) {
|
|
169
|
+
fields.push(`"alg": "${consumer.alg}"`);
|
|
170
|
+
}
|
|
171
|
+
return fields.join('\n');
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Verify AgenticConsumer object signature and nonce linkage
|
|
175
|
+
* Main verification function for TAP Layer 2
|
|
176
|
+
*/
|
|
177
|
+
export async function verifyAgenticConsumer(consumer, headerNonce, publicKey, algorithm) {
|
|
178
|
+
try {
|
|
179
|
+
// Step 1: Check nonce linkage
|
|
180
|
+
const nonceLinked = consumer.nonce === headerNonce;
|
|
181
|
+
// Step 2: Build signature base
|
|
182
|
+
const signatureBase = buildConsumerSignatureBase(consumer);
|
|
183
|
+
// Step 3: Verify signature
|
|
184
|
+
const alg = algorithm || consumer.alg;
|
|
185
|
+
const signatureValid = await verifyCryptoSignature(signatureBase, consumer.signature, publicKey, alg);
|
|
186
|
+
// Step 4: Parse and validate ID token if present
|
|
187
|
+
let idTokenValid;
|
|
188
|
+
let idTokenClaims;
|
|
189
|
+
if (consumer.idToken) {
|
|
190
|
+
const tokenString = typeof consumer.idToken === 'string'
|
|
191
|
+
? consumer.idToken
|
|
192
|
+
: consumer.idToken.signature; // If already parsed
|
|
193
|
+
if (tokenString) {
|
|
194
|
+
const parsedClaims = parseIDToken(tokenString);
|
|
195
|
+
idTokenClaims = parsedClaims ?? undefined;
|
|
196
|
+
idTokenValid = parsedClaims !== null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const verified = nonceLinked && signatureValid;
|
|
200
|
+
return {
|
|
201
|
+
verified,
|
|
202
|
+
nonceLinked,
|
|
203
|
+
signatureValid,
|
|
204
|
+
idTokenValid,
|
|
205
|
+
idTokenClaims,
|
|
206
|
+
contextualData: consumer.contextualData,
|
|
207
|
+
error: verified ? undefined : 'Verification failed'
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
return {
|
|
212
|
+
verified: false,
|
|
213
|
+
nonceLinked: false,
|
|
214
|
+
signatureValid: false,
|
|
215
|
+
error: `Verification error: ${error instanceof Error ? error.message : 'Unknown'}`
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Verify cryptographic signature using Web Crypto API
|
|
221
|
+
*/
|
|
222
|
+
async function verifyCryptoSignature(signatureBase, signature, publicKeyPem, algorithm) {
|
|
223
|
+
try {
|
|
224
|
+
// Decode signature from base64
|
|
225
|
+
const signatureBytes = Uint8Array.from(atob(signature), c => c.charCodeAt(0));
|
|
226
|
+
// Import public key
|
|
227
|
+
const keyData = pemToArrayBuffer(publicKeyPem);
|
|
228
|
+
const { importParams, verifyParams } = jwkAlgToWebCrypto(algorithm);
|
|
229
|
+
const cryptoKey = await crypto.subtle.importKey('spki', keyData, importParams, false, ['verify']);
|
|
230
|
+
// Verify signature
|
|
231
|
+
const encoder = new TextEncoder();
|
|
232
|
+
const data = encoder.encode(signatureBase);
|
|
233
|
+
return await crypto.subtle.verify(verifyParams, cryptoKey, signatureBytes, data);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
console.error('Consumer signature verification error:', error);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// ============ IDENTITY MATCHING ============
|
|
241
|
+
/**
|
|
242
|
+
* Hash-match obfuscated identity with cleartext
|
|
243
|
+
* Merchants maintain mapping tables with hashed values
|
|
244
|
+
*/
|
|
245
|
+
export async function hashMatchIdentity(obfuscated, cleartext, method = 'sha256') {
|
|
246
|
+
try {
|
|
247
|
+
const encoder = new TextEncoder();
|
|
248
|
+
const data = encoder.encode(cleartext);
|
|
249
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
250
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
251
|
+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
252
|
+
// Compare with obfuscated value (assume it's hex-encoded hash)
|
|
253
|
+
return hashHex === obfuscated.toLowerCase();
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// ============ CRYPTO HELPERS ============
|
|
260
|
+
/**
|
|
261
|
+
* Convert PEM public key to ArrayBuffer
|
|
262
|
+
*/
|
|
263
|
+
function pemToArrayBuffer(pem) {
|
|
264
|
+
const base64 = pem
|
|
265
|
+
.replace(/-----BEGIN PUBLIC KEY-----/, '')
|
|
266
|
+
.replace(/-----END PUBLIC KEY-----/, '')
|
|
267
|
+
.replace(/\s/g, '');
|
|
268
|
+
const binary = atob(base64);
|
|
269
|
+
const bytes = new Uint8Array(binary.length);
|
|
270
|
+
for (let i = 0; i < binary.length; i++) {
|
|
271
|
+
bytes[i] = binary.charCodeAt(i);
|
|
272
|
+
}
|
|
273
|
+
return bytes.buffer;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Convert base64url string to ArrayBuffer
|
|
277
|
+
*/
|
|
278
|
+
function base64UrlToArrayBuffer(base64url) {
|
|
279
|
+
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
|
|
280
|
+
const binary = atob(base64);
|
|
281
|
+
const bytes = new Uint8Array(binary.length);
|
|
282
|
+
for (let i = 0; i < binary.length; i++) {
|
|
283
|
+
bytes[i] = binary.charCodeAt(i);
|
|
284
|
+
}
|
|
285
|
+
return bytes.buffer;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Convert JWK algorithm name to Web Crypto API parameters
|
|
289
|
+
*/
|
|
290
|
+
function jwkAlgToWebCrypto(alg) {
|
|
291
|
+
switch (alg) {
|
|
292
|
+
case 'PS256':
|
|
293
|
+
return {
|
|
294
|
+
importParams: { name: 'RSA-PSS', hash: 'SHA-256' },
|
|
295
|
+
verifyParams: { name: 'RSA-PSS', saltLength: 32 }
|
|
296
|
+
};
|
|
297
|
+
case 'ES256':
|
|
298
|
+
return {
|
|
299
|
+
importParams: { name: 'ECDSA', namedCurve: 'P-256' },
|
|
300
|
+
verifyParams: { name: 'ECDSA', hash: 'SHA-256' }
|
|
301
|
+
};
|
|
302
|
+
case 'EdDSA':
|
|
303
|
+
return {
|
|
304
|
+
importParams: { name: 'Ed25519' },
|
|
305
|
+
verifyParams: { name: 'Ed25519' }
|
|
306
|
+
};
|
|
307
|
+
case 'RS256':
|
|
308
|
+
return {
|
|
309
|
+
importParams: { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
|
|
310
|
+
verifyParams: { name: 'RSASSA-PKCS1-v1_5' }
|
|
311
|
+
};
|
|
312
|
+
default:
|
|
313
|
+
throw new Error(`Unsupported algorithm: ${alg}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Convert JWK to CryptoKey
|
|
318
|
+
*/
|
|
319
|
+
async function jwkToCryptoKey(jwk, alg) {
|
|
320
|
+
const { importParams } = jwkAlgToWebCrypto(alg);
|
|
321
|
+
// Build JWK for import
|
|
322
|
+
const keyData = {
|
|
323
|
+
kty: jwk.kty,
|
|
324
|
+
alg: alg,
|
|
325
|
+
ext: true
|
|
326
|
+
};
|
|
327
|
+
if (jwk.kty === 'RSA') {
|
|
328
|
+
keyData.n = jwk.n;
|
|
329
|
+
keyData.e = jwk.e;
|
|
330
|
+
}
|
|
331
|
+
else if (jwk.kty === 'EC') {
|
|
332
|
+
keyData.crv = jwk.crv;
|
|
333
|
+
keyData.x = jwk.x;
|
|
334
|
+
keyData.y = jwk.y;
|
|
335
|
+
}
|
|
336
|
+
return await crypto.subtle.importKey('jwk', keyData, importParams, true, ['verify']);
|
|
337
|
+
}
|
|
338
|
+
// ============ EXPORTS ============
|
|
339
|
+
export default {
|
|
340
|
+
parseAgenticConsumer,
|
|
341
|
+
verifyAgenticConsumer,
|
|
342
|
+
parseIDToken,
|
|
343
|
+
verifyIDTokenSignature,
|
|
344
|
+
hashMatchIdentity,
|
|
345
|
+
buildConsumerSignatureBase
|
|
346
|
+
};
|