@safepassage/sdk 3.4.9 → 3.4.10
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/README.md +14 -10
- package/brands/safepassage/index.d.ts +12 -0
- package/brands/safepassage/urls.d.ts +3 -0
- package/core/VerificationSDK.d.ts +132 -0
- package/index.d.ts +2 -0
- package/index.js +1091 -0
- package/package.json +14 -47
- package/safepassage.min.js +3 -0
- package/sdk.min.js +3 -0
- package/{dist/types/index.d.ts → types/base.d.ts} +27 -10
- package/utils/crypto.d.ts +31 -0
- package/utils/environment.d.ts +8 -0
- package/{dist/utils → utils}/polyfills.d.ts +1 -2
- package/{dist/utils → utils}/security.d.ts +11 -11
- package/{dist/utils → utils}/validation.d.ts +9 -12
- package/dist/core/SafePassageSDK.d.ts +0 -231
- package/dist/core/SafePassageSDK.js +0 -656
- package/dist/index.d.ts +0 -21
- package/dist/index.js +0 -1100
- package/dist/safepassage.min.js +0 -3
- package/dist/types/index.js +0 -4
- package/dist/utils/__mocks__/polyfills.d.ts +0 -3
- package/dist/utils/__mocks__/polyfills.js +0 -10
- package/dist/utils/crypto.d.ts +0 -105
- package/dist/utils/crypto.js +0 -216
- package/dist/utils/environment.d.ts +0 -13
- package/dist/utils/environment.js +0 -75
- package/dist/utils/polyfills.js +0 -58
- package/dist/utils/security.js +0 -190
- package/dist/utils/validation.js +0 -158
package/index.js
ADDED
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defProps = Object.defineProperties;
|
|
3
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
8
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
+
var __spreadValues = (a, b) => {
|
|
10
|
+
for (var prop in b || (b = {}))
|
|
11
|
+
if (__hasOwnProp.call(b, prop))
|
|
12
|
+
__defNormalProp(a, prop, b[prop]);
|
|
13
|
+
if (__getOwnPropSymbols)
|
|
14
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
15
|
+
if (__propIsEnum.call(b, prop))
|
|
16
|
+
__defNormalProp(a, prop, b[prop]);
|
|
17
|
+
}
|
|
18
|
+
return a;
|
|
19
|
+
};
|
|
20
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
21
|
+
var __objRest = (source, exclude) => {
|
|
22
|
+
var target = {};
|
|
23
|
+
for (var prop in source)
|
|
24
|
+
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
25
|
+
target[prop] = source[prop];
|
|
26
|
+
if (source != null && __getOwnPropSymbols)
|
|
27
|
+
for (var prop of __getOwnPropSymbols(source)) {
|
|
28
|
+
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
29
|
+
target[prop] = source[prop];
|
|
30
|
+
}
|
|
31
|
+
return target;
|
|
32
|
+
};
|
|
33
|
+
var __esm = (fn, res) => function __init() {
|
|
34
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
35
|
+
};
|
|
36
|
+
var __export = (target, all) => {
|
|
37
|
+
for (var name in all)
|
|
38
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src-redirect/utils/security.ts
|
|
42
|
+
function isOriginTrusted(origin, trustedOrigins) {
|
|
43
|
+
return trustedOrigins.includes(origin);
|
|
44
|
+
}
|
|
45
|
+
function validatePostMessageOrigin(event, trustedOrigins, allowedCustomOrigins = [], logLabel = "SDK") {
|
|
46
|
+
var _a;
|
|
47
|
+
const { origin } = event;
|
|
48
|
+
if (isOriginTrusted(origin, trustedOrigins)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (allowedCustomOrigins.length > 0) {
|
|
52
|
+
const isCustomOriginAllowed = allowedCustomOrigins.some((allowedOrigin) => {
|
|
53
|
+
if (allowedOrigin.startsWith("*.")) {
|
|
54
|
+
const domain = allowedOrigin.slice(2);
|
|
55
|
+
return origin.endsWith(`.${domain}`) || origin === `https://${domain}` || origin === `http://${domain}`;
|
|
56
|
+
}
|
|
57
|
+
return origin === allowedOrigin;
|
|
58
|
+
});
|
|
59
|
+
if (isCustomOriginAllowed) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
console.warn(
|
|
64
|
+
`${logLabel} Security: Blocked PostMessage from untrusted origin: ${origin}`,
|
|
65
|
+
{
|
|
66
|
+
trustedOrigins,
|
|
67
|
+
allowedCustomOrigins,
|
|
68
|
+
eventType: (_a = event.data) == null ? void 0 : _a.type
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
function validateVerificationMessage(event, expectedSessionId, expectedMessageType, legacyMessageType) {
|
|
74
|
+
const { data } = event;
|
|
75
|
+
if (!data || typeof data !== "object") {
|
|
76
|
+
return { isValid: false, error: "Invalid message format" };
|
|
77
|
+
}
|
|
78
|
+
const allowedTypes = legacyMessageType ? [expectedMessageType, legacyMessageType] : [expectedMessageType];
|
|
79
|
+
if (!allowedTypes.includes(data.type)) {
|
|
80
|
+
return { isValid: false, error: "Invalid message type" };
|
|
81
|
+
}
|
|
82
|
+
if (!data.sessionId || data.sessionId !== expectedSessionId) {
|
|
83
|
+
return { isValid: false, error: "Session ID mismatch" };
|
|
84
|
+
}
|
|
85
|
+
if (!data.status || !["verified", "failed", "cancelled"].includes(data.status)) {
|
|
86
|
+
return { isValid: false, error: "Invalid status value" };
|
|
87
|
+
}
|
|
88
|
+
return { isValid: true };
|
|
89
|
+
}
|
|
90
|
+
function enforceHTTPS(environment, logLabel = "SDK") {
|
|
91
|
+
if (environment === "production" && window.location.protocol !== "https:") {
|
|
92
|
+
console.warn(
|
|
93
|
+
`${logLabel} Warning: HTTPS recommended for production environment`,
|
|
94
|
+
{ current: window.location.href }
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function validateReturnUrl(url, environment, _logLabel = "SDK") {
|
|
99
|
+
try {
|
|
100
|
+
const parsed = new URL(url);
|
|
101
|
+
if (parsed.protocol !== "https:") {
|
|
102
|
+
const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
103
|
+
if (!isLocalhost) {
|
|
104
|
+
return {
|
|
105
|
+
isValid: false,
|
|
106
|
+
error: `HTTPS required for return URLs in ${environment}`
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const suspiciousPatterns = [
|
|
111
|
+
/data:/i,
|
|
112
|
+
/javascript:/i,
|
|
113
|
+
/vbscript:/i,
|
|
114
|
+
/file:/i,
|
|
115
|
+
/ftp:/i
|
|
116
|
+
];
|
|
117
|
+
for (const pattern of suspiciousPatterns) {
|
|
118
|
+
if (pattern.test(url)) {
|
|
119
|
+
return { isValid: false, error: "Blocked suspicious URL scheme" };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { isValid: true };
|
|
123
|
+
} catch (e) {
|
|
124
|
+
return { isValid: false, error: "Invalid URL format" };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function logSecurityEvent(event, metadata, logLabel = "SDK") {
|
|
128
|
+
const context = {
|
|
129
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
130
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "unknown",
|
|
131
|
+
url: typeof window !== "undefined" ? window.location.href : "unknown"
|
|
132
|
+
};
|
|
133
|
+
console.warn(`${logLabel} Security Event: ${event}`, __spreadValues(__spreadValues({}, context), metadata));
|
|
134
|
+
}
|
|
135
|
+
var VerificationRateLimit, verificationRateLimit;
|
|
136
|
+
var init_security = __esm({
|
|
137
|
+
"src-redirect/utils/security.ts"() {
|
|
138
|
+
"use strict";
|
|
139
|
+
VerificationRateLimit = class {
|
|
140
|
+
constructor() {
|
|
141
|
+
this.attempts = /* @__PURE__ */ new Map();
|
|
142
|
+
this.maxAttempts = 5;
|
|
143
|
+
this.timeWindow = 6e4;
|
|
144
|
+
}
|
|
145
|
+
// 1 minute
|
|
146
|
+
isAllowed(identifier, logLabel = "SDK") {
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
const attempts = this.attempts.get(identifier) || [];
|
|
149
|
+
const recentAttempts = attempts.filter(
|
|
150
|
+
(time) => now - time < this.timeWindow
|
|
151
|
+
);
|
|
152
|
+
if (recentAttempts.length >= this.maxAttempts) {
|
|
153
|
+
console.warn(
|
|
154
|
+
`${logLabel} Security: Rate limit exceeded for ${identifier}`
|
|
155
|
+
);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
recentAttempts.push(now);
|
|
159
|
+
this.attempts.set(identifier, recentAttempts);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
reset(identifier) {
|
|
163
|
+
this.attempts.delete(identifier);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
verificationRateLimit = new VerificationRateLimit();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// src-redirect/utils/crypto.ts
|
|
171
|
+
var crypto_exports = {};
|
|
172
|
+
__export(crypto_exports, {
|
|
173
|
+
createSignedState: () => createSignedState,
|
|
174
|
+
generateHMAC: () => generateHMAC,
|
|
175
|
+
generateSecureToken: () => generateSecureToken,
|
|
176
|
+
parseSignedState: () => parseSignedState,
|
|
177
|
+
verifyHMAC: () => verifyHMAC
|
|
178
|
+
});
|
|
179
|
+
async function generateHMAC(data, secret) {
|
|
180
|
+
const encoder = new TextEncoder();
|
|
181
|
+
const keyData = encoder.encode(secret);
|
|
182
|
+
const dataBuffer = encoder.encode(data);
|
|
183
|
+
const key = await crypto.subtle.importKey(
|
|
184
|
+
"raw",
|
|
185
|
+
keyData,
|
|
186
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
187
|
+
false,
|
|
188
|
+
["sign"]
|
|
189
|
+
);
|
|
190
|
+
const signature = await crypto.subtle.sign("HMAC", key, dataBuffer);
|
|
191
|
+
return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
192
|
+
}
|
|
193
|
+
async function verifyHMAC(data, signature, secret) {
|
|
194
|
+
try {
|
|
195
|
+
const expectedSignature = await generateHMAC(data, secret);
|
|
196
|
+
return constantTimeCompare(signature, expectedSignature);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function constantTimeCompare(a, b) {
|
|
202
|
+
if (a.length !== b.length) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
let result = 0;
|
|
206
|
+
for (let i = 0; i < a.length; i++) {
|
|
207
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
208
|
+
}
|
|
209
|
+
return result === 0;
|
|
210
|
+
}
|
|
211
|
+
function generateSecureToken(length = 32) {
|
|
212
|
+
const array = new Uint8Array(length);
|
|
213
|
+
crypto.getRandomValues(array);
|
|
214
|
+
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
215
|
+
}
|
|
216
|
+
async function createSignedState(payload, hmacSecret) {
|
|
217
|
+
const timestampedPayload = __spreadProps(__spreadValues({}, payload), {
|
|
218
|
+
timestamp: Date.now(),
|
|
219
|
+
nonce: generateSecureToken(16)
|
|
220
|
+
});
|
|
221
|
+
const dataString = JSON.stringify(timestampedPayload);
|
|
222
|
+
const signature = await generateHMAC(dataString, hmacSecret);
|
|
223
|
+
const signedPayload = {
|
|
224
|
+
data: timestampedPayload,
|
|
225
|
+
signature
|
|
226
|
+
};
|
|
227
|
+
return btoa(JSON.stringify(signedPayload));
|
|
228
|
+
}
|
|
229
|
+
async function parseSignedState(signedState, hmacSecret, maxAge = STATE_EXPIRY_MS, logLabel = "SDK") {
|
|
230
|
+
try {
|
|
231
|
+
const json = atob(signedState);
|
|
232
|
+
const signedPayload = JSON.parse(json);
|
|
233
|
+
if (!signedPayload.data || !signedPayload.signature) {
|
|
234
|
+
console.warn(`${logLabel}: Invalid signed state format`);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const { data, signature } = signedPayload;
|
|
238
|
+
const dataString = JSON.stringify(data);
|
|
239
|
+
const isValid = await verifyHMAC(dataString, signature, hmacSecret);
|
|
240
|
+
if (!isValid) {
|
|
241
|
+
console.warn(`${logLabel}: State signature verification failed`);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
if (data.timestamp) {
|
|
245
|
+
const age = Date.now() - data.timestamp;
|
|
246
|
+
if (age > maxAge) {
|
|
247
|
+
console.warn(`${logLabel}: State parameter expired`, { age, maxAge });
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const _a = data, { timestamp, nonce } = _a, payload = __objRest(_a, ["timestamp", "nonce"]);
|
|
252
|
+
void timestamp;
|
|
253
|
+
void nonce;
|
|
254
|
+
return payload;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.warn(`${logLabel}: Failed to parse signed state`, error);
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
var init_crypto = __esm({
|
|
261
|
+
"src-redirect/utils/crypto.ts"() {
|
|
262
|
+
"use strict";
|
|
263
|
+
init_validation();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// src-redirect/utils/validation.ts
|
|
268
|
+
function validateConfig(config, context) {
|
|
269
|
+
if (!config.apiKey) {
|
|
270
|
+
throw new Error("apiKey is required");
|
|
271
|
+
}
|
|
272
|
+
if (config.apiKey.length > MAX_API_KEY_LENGTH) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`apiKey exceeds maximum length of ${MAX_API_KEY_LENGTH} characters`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (!API_KEY_PATTERN.test(config.apiKey)) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
"Invalid apiKey format. Expected pk_xxx (public) or sk_xxx (private)"
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (typeof window !== "undefined" && config.apiKey.startsWith("sk_")) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`Secret keys (sk_) should never be used in browser code for security reasons. Secret keys expose your account to unauthorized access if used client-side. Please use your public key (pk_) instead. If you need to use features that require a secret key (like custom challenge age), create the session server-side and pass the sessionId to startVerificationWithSession(). See: ${context.docsUrl}/server-side-sessions`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
if (!config.returnUrl) {
|
|
288
|
+
throw new Error("returnUrl is required");
|
|
289
|
+
}
|
|
290
|
+
if (config.returnUrl.length > MAX_URL_LENGTH) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`returnUrl exceeds maximum length of ${MAX_URL_LENGTH} characters`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const environment = detectEnvironment();
|
|
296
|
+
const returnUrlValidation = validateReturnUrl(config.returnUrl, environment, context.brandName);
|
|
297
|
+
if (!returnUrlValidation.isValid) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`returnUrl validation failed: ${returnUrlValidation.error}`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (config.cancelUrl) {
|
|
303
|
+
if (config.cancelUrl.length > MAX_URL_LENGTH) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`cancelUrl exceeds maximum length of ${MAX_URL_LENGTH} characters`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
const cancelUrlValidation = validateReturnUrl(config.cancelUrl, environment, context.brandName);
|
|
309
|
+
if (!cancelUrlValidation.isValid) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`cancelUrl validation failed: ${cancelUrlValidation.error}`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (config.defaultChallengeAge !== void 0) {
|
|
316
|
+
if (config.defaultChallengeAge < MINIMUM_AGE) {
|
|
317
|
+
throw new Error(`defaultChallengeAge must be at least ${MINIMUM_AGE}`);
|
|
318
|
+
}
|
|
319
|
+
if (config.defaultChallengeAge > MAXIMUM_AGE) {
|
|
320
|
+
throw new Error(`defaultChallengeAge cannot exceed ${MAXIMUM_AGE}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (config.defaultVerificationMode && !["L1", "L2"].includes(config.defaultVerificationMode)) {
|
|
324
|
+
throw new Error("defaultVerificationMode must be L1 or L2");
|
|
325
|
+
}
|
|
326
|
+
if (config.mode && !["redirect", "new-tab"].includes(config.mode)) {
|
|
327
|
+
throw new Error("mode must be redirect or new-tab");
|
|
328
|
+
}
|
|
329
|
+
if (config.newTabTarget && !["popup", "tab"].includes(config.newTabTarget)) {
|
|
330
|
+
throw new Error("newTabTarget must be popup or tab");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function detectEnvironment() {
|
|
334
|
+
if (typeof window === "undefined") {
|
|
335
|
+
return "production";
|
|
336
|
+
}
|
|
337
|
+
const hostname = window.location.hostname;
|
|
338
|
+
if (hostname.includes("staging") || hostname.includes("stage")) {
|
|
339
|
+
return "staging";
|
|
340
|
+
}
|
|
341
|
+
return "production";
|
|
342
|
+
}
|
|
343
|
+
async function generateState(payload, environment, hmacSecret, _logLabel = "SDK") {
|
|
344
|
+
const { createSignedState: createSignedState2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
345
|
+
return createSignedState2(payload, hmacSecret);
|
|
346
|
+
}
|
|
347
|
+
var MINIMUM_AGE, MAXIMUM_AGE, MAX_URL_LENGTH, MAX_API_KEY_LENGTH, STATE_EXPIRY_MS, API_KEY_PATTERN;
|
|
348
|
+
var init_validation = __esm({
|
|
349
|
+
"src-redirect/utils/validation.ts"() {
|
|
350
|
+
"use strict";
|
|
351
|
+
init_security();
|
|
352
|
+
MINIMUM_AGE = 25;
|
|
353
|
+
MAXIMUM_AGE = 150;
|
|
354
|
+
MAX_URL_LENGTH = 2048;
|
|
355
|
+
MAX_API_KEY_LENGTH = 128;
|
|
356
|
+
STATE_EXPIRY_MS = 6e5;
|
|
357
|
+
API_KEY_PATTERN = /^(pk_|sk_)[a-zA-Z0-9_]+$/;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// src-redirect/utils/polyfills.ts
|
|
362
|
+
function setupPolyfills() {
|
|
363
|
+
if (!crypto.randomUUID) {
|
|
364
|
+
crypto.randomUUID = function() {
|
|
365
|
+
const array = new Uint8Array(16);
|
|
366
|
+
crypto.getRandomValues(array);
|
|
367
|
+
array[6] = array[6] & 15 | 64;
|
|
368
|
+
array[8] = array[8] & 63 | 128;
|
|
369
|
+
const hex = Array.from(array).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
370
|
+
return [
|
|
371
|
+
hex.slice(0, 8),
|
|
372
|
+
hex.slice(8, 12),
|
|
373
|
+
hex.slice(12, 16),
|
|
374
|
+
hex.slice(16, 20),
|
|
375
|
+
hex.slice(20, 32)
|
|
376
|
+
].join("-");
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function checkBrowserCompatibility(logLabel = "SDK") {
|
|
381
|
+
const warnings = [];
|
|
382
|
+
if (!window.crypto || !window.crypto.getRandomValues) {
|
|
383
|
+
throw new Error(`${logLabel} requires Web Crypto API support`);
|
|
384
|
+
}
|
|
385
|
+
if (!window.crypto.subtle) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`${logLabel} requires Web Crypto subtle API for HMAC operations`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
if (!crypto.randomUUID) {
|
|
391
|
+
warnings.push("crypto.randomUUID not supported, using polyfill");
|
|
392
|
+
}
|
|
393
|
+
if (!window.URLSearchParams) {
|
|
394
|
+
warnings.push(
|
|
395
|
+
"URLSearchParams not supported, consider adding a polyfill for IE 11 support"
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
if (warnings.length > 0) {
|
|
399
|
+
console.warn(`${logLabel} Browser Compatibility:`, warnings.join("; "));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src-redirect/core/VerificationSDK.ts
|
|
404
|
+
init_validation();
|
|
405
|
+
|
|
406
|
+
// src-redirect/utils/environment.ts
|
|
407
|
+
function getEnvironmentUrl(environment, urls) {
|
|
408
|
+
const url = urls.verifyUiUrl;
|
|
409
|
+
if (!url || !url.startsWith("https://")) {
|
|
410
|
+
throw new Error(`HTTPS required for ${environment} environment`);
|
|
411
|
+
}
|
|
412
|
+
return url;
|
|
413
|
+
}
|
|
414
|
+
function getApiUrl(environment, urls) {
|
|
415
|
+
const url = urls.apiUrl;
|
|
416
|
+
if (!url || !url.startsWith("https://")) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
`HTTPS required for API URLs in ${environment} environment`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
return url;
|
|
422
|
+
}
|
|
423
|
+
function validateEnvironmentSecurity(environment, urls, logLabel = "SDK") {
|
|
424
|
+
const isSecure = window.location.protocol === "https:";
|
|
425
|
+
switch (environment) {
|
|
426
|
+
case "production":
|
|
427
|
+
if (!isSecure) {
|
|
428
|
+
console.warn(`${logLabel} Warning: HTTPS recommended for production environment`);
|
|
429
|
+
}
|
|
430
|
+
break;
|
|
431
|
+
case "staging":
|
|
432
|
+
if (!isSecure) {
|
|
433
|
+
console.warn(
|
|
434
|
+
`${logLabel} Warning: HTTPS strongly recommended in staging environment`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
getEnvironmentUrl(environment, urls);
|
|
441
|
+
getApiUrl(environment, urls);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
444
|
+
throw new Error(`Environment configuration validation failed: ${errorMessage}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src-redirect/core/VerificationSDK.ts
|
|
449
|
+
init_security();
|
|
450
|
+
var _VerificationSDK = class _VerificationSDK {
|
|
451
|
+
/**
|
|
452
|
+
* Initialize SDK
|
|
453
|
+
*
|
|
454
|
+
* Validates configuration, sets up security measures, and prepares the SDK
|
|
455
|
+
* for verification operations. Performs comprehensive environment validation
|
|
456
|
+
* and security initialization.
|
|
457
|
+
*/
|
|
458
|
+
constructor(config, brandUrls, brandConstants) {
|
|
459
|
+
this.popupWindow = null;
|
|
460
|
+
this.messageListener = null;
|
|
461
|
+
this.popupMonitorInterval = null;
|
|
462
|
+
this.unloadListener = null;
|
|
463
|
+
this.isVerificationInProgress = false;
|
|
464
|
+
this.currentSessionId = null;
|
|
465
|
+
this.hasReceivedResult = false;
|
|
466
|
+
// Server-provided verify URL (includes sessionToken)
|
|
467
|
+
this.lastVerifyUrl = null;
|
|
468
|
+
// Server-provided session token (WS auth)
|
|
469
|
+
this.lastSessionToken = null;
|
|
470
|
+
// External user ID provided during verify() for cancellation redirects
|
|
471
|
+
this.lastExternalUserId = null;
|
|
472
|
+
// Temporary storage for QR handoff token to include in state
|
|
473
|
+
this.temporaryHandoffToken = null;
|
|
474
|
+
this.brandUrls = brandUrls;
|
|
475
|
+
this.brandConstants = brandConstants;
|
|
476
|
+
validateConfig(config, {
|
|
477
|
+
brandName: this.brandConstants.name,
|
|
478
|
+
docsUrl: this.brandConstants.docsUrl
|
|
479
|
+
});
|
|
480
|
+
let normalizedEnvironment = config.environment || this.detectEnvironment();
|
|
481
|
+
if (normalizedEnvironment !== "staging" && normalizedEnvironment !== "production") {
|
|
482
|
+
console.warn(
|
|
483
|
+
`${this.brandConstants.name} SDK: Unknown environment '${normalizedEnvironment}', defaulting to 'production'`
|
|
484
|
+
);
|
|
485
|
+
normalizedEnvironment = "production";
|
|
486
|
+
}
|
|
487
|
+
this.config = __spreadProps(__spreadValues({}, config), {
|
|
488
|
+
environment: normalizedEnvironment,
|
|
489
|
+
mode: config.mode || "redirect",
|
|
490
|
+
newTabTarget: config.newTabTarget || "popup"
|
|
491
|
+
});
|
|
492
|
+
validateEnvironmentSecurity(this.config.environment, this.getUrlConfig(), this.brandConstants.name);
|
|
493
|
+
enforceHTTPS(this.config.environment, this.brandConstants.name);
|
|
494
|
+
logSecurityEvent("SDK_INITIALIZED", {
|
|
495
|
+
environment: this.config.environment,
|
|
496
|
+
mode: this.config.mode,
|
|
497
|
+
origin: window.location.origin,
|
|
498
|
+
protocol: window.location.protocol,
|
|
499
|
+
hostname: window.location.hostname
|
|
500
|
+
}, this.brandConstants.name);
|
|
501
|
+
this.setupAutoCleanup();
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Initiate verification with race condition protection
|
|
505
|
+
*/
|
|
506
|
+
async verify(options = {}) {
|
|
507
|
+
var _a, _b, _c, _d, _e, _f;
|
|
508
|
+
const isPublicKey = this.isPublicKey();
|
|
509
|
+
let sessionId;
|
|
510
|
+
if (isPublicKey) {
|
|
511
|
+
sessionId = await this.createInternalSession(options);
|
|
512
|
+
} else {
|
|
513
|
+
throw new Error(
|
|
514
|
+
"Private API keys (sk_) should use the direct API, not the SDK. The SDK is designed for browser-based public key usage only."
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
if (!sessionId) {
|
|
518
|
+
throw new Error("Failed to obtain sessionId from server");
|
|
519
|
+
}
|
|
520
|
+
if (this.isVerificationInProgress) {
|
|
521
|
+
const error = new Error(
|
|
522
|
+
`Verification already in progress for session ${(_a = this.currentSessionId) == null ? void 0 : _a.substring(0, 8)}...`
|
|
523
|
+
);
|
|
524
|
+
logSecurityEvent("RACE_CONDITION_PREVENTED", {
|
|
525
|
+
currentSession: ((_b = this.currentSessionId) == null ? void 0 : _b.substring(0, 8)) + "...",
|
|
526
|
+
attemptedSession: "new-session-attempt",
|
|
527
|
+
origin: window.location.origin
|
|
528
|
+
}, this.brandConstants.name);
|
|
529
|
+
(_d = (_c = this.config).onError) == null ? void 0 : _d.call(_c, error);
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
this.isVerificationInProgress = true;
|
|
533
|
+
this.currentSessionId = sessionId;
|
|
534
|
+
this.lastExternalUserId = options.externalUserId || null;
|
|
535
|
+
try {
|
|
536
|
+
const rateLimitKey = `${this.config.apiKey}:${window.location.origin}`;
|
|
537
|
+
if (!verificationRateLimit.isAllowed(rateLimitKey, this.brandConstants.name)) {
|
|
538
|
+
const error = new Error(
|
|
539
|
+
"Too many verification attempts. Please wait before trying again."
|
|
540
|
+
);
|
|
541
|
+
logSecurityEvent("RATE_LIMIT_EXCEEDED", {
|
|
542
|
+
apiKey: this.config.apiKey.substring(0, 8) + "...",
|
|
543
|
+
origin: window.location.origin,
|
|
544
|
+
sessionId: sessionId ? sessionId.substring(0, 8) + "..." : "undefined"
|
|
545
|
+
}, this.brandConstants.name);
|
|
546
|
+
(_f = (_e = this.config).onError) == null ? void 0 : _f.call(_e, error);
|
|
547
|
+
throw error;
|
|
548
|
+
}
|
|
549
|
+
const verificationUrl = await this.buildVerificationUrl(
|
|
550
|
+
options,
|
|
551
|
+
sessionId
|
|
552
|
+
);
|
|
553
|
+
logSecurityEvent("VERIFICATION_INITIATED", {
|
|
554
|
+
environment: this.config.environment,
|
|
555
|
+
mode: this.config.mode,
|
|
556
|
+
sessionId: sessionId ? sessionId.substring(0, 8) + "..." : "undefined",
|
|
557
|
+
origin: window.location.origin
|
|
558
|
+
}, this.brandConstants.name);
|
|
559
|
+
if (this.config.mode === "new-tab") {
|
|
560
|
+
this.openNewTab(verificationUrl, sessionId);
|
|
561
|
+
} else {
|
|
562
|
+
this.unlockVerification();
|
|
563
|
+
this.redirect(verificationUrl);
|
|
564
|
+
}
|
|
565
|
+
} catch (error) {
|
|
566
|
+
this.unlockVerification();
|
|
567
|
+
throw error;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Build verification URL with HMAC-signed state
|
|
572
|
+
*/
|
|
573
|
+
async buildVerificationUrl(options, sessionId) {
|
|
574
|
+
const baseUrl = this.config.verifyUrl || getEnvironmentUrl(this.config.environment, this.getUrlConfig());
|
|
575
|
+
const hasExplicitChallengeAge = options.challengeAge !== void 0;
|
|
576
|
+
const hasExplicitVerificationMode = options.verificationMode !== void 0;
|
|
577
|
+
const hasOverrides = hasExplicitChallengeAge || hasExplicitVerificationMode;
|
|
578
|
+
const state = await generateState(
|
|
579
|
+
{
|
|
580
|
+
merchantId: this.config.apiKey,
|
|
581
|
+
sessionId,
|
|
582
|
+
returnUrl: this.config.returnUrl,
|
|
583
|
+
cancelUrl: this.config.cancelUrl,
|
|
584
|
+
challengeAge: options.challengeAge || this.config.defaultChallengeAge,
|
|
585
|
+
verificationMode: options.verificationMode || this.config.defaultVerificationMode,
|
|
586
|
+
hasOverrides,
|
|
587
|
+
// Flag to indicate explicit overrides
|
|
588
|
+
externalUserId: options.externalUserId,
|
|
589
|
+
timestamp: Date.now(),
|
|
590
|
+
// Additional config for self-contained verify-ui
|
|
591
|
+
apiUrl: this.getPortalApiUrl(),
|
|
592
|
+
engineUrl: this.getEngineUrl(),
|
|
593
|
+
wsUrl: this.getWebSocketUrl(),
|
|
594
|
+
environment: this.config.environment,
|
|
595
|
+
features: {
|
|
596
|
+
testMode: false,
|
|
597
|
+
warmupPeriodMs: 500,
|
|
598
|
+
qualityThreshold: 0.6
|
|
599
|
+
},
|
|
600
|
+
// Include handoffToken if available (for QR code desktop flow)
|
|
601
|
+
handoffToken: this.temporaryHandoffToken || void 0,
|
|
602
|
+
// Include sessionToken and verifyUrl to make UI auth deterministic
|
|
603
|
+
sessionToken: this.lastSessionToken || void 0,
|
|
604
|
+
verifyUrl: this.lastVerifyUrl || void 0
|
|
605
|
+
},
|
|
606
|
+
this.config.environment,
|
|
607
|
+
this.getHmacSecret(),
|
|
608
|
+
this.brandConstants.name
|
|
609
|
+
);
|
|
610
|
+
if (this.lastVerifyUrl) {
|
|
611
|
+
try {
|
|
612
|
+
const resolvedVerifyUrl = this.applyLocalVerifyOverride(this.lastVerifyUrl);
|
|
613
|
+
const url = new URL(resolvedVerifyUrl);
|
|
614
|
+
url.searchParams.set("state", state);
|
|
615
|
+
url.searchParams.set("mode", this.config.mode);
|
|
616
|
+
if (options.skipIntro) {
|
|
617
|
+
url.searchParams.set("skip_intro", "true");
|
|
618
|
+
}
|
|
619
|
+
if (options.autoReturn) {
|
|
620
|
+
url.searchParams.set("auto_return", "true");
|
|
621
|
+
}
|
|
622
|
+
return url.toString();
|
|
623
|
+
} catch (e) {
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const params = new URLSearchParams({ state, sessionId, mode: this.config.mode });
|
|
627
|
+
if (options.skipIntro) {
|
|
628
|
+
params.set("skip_intro", "true");
|
|
629
|
+
}
|
|
630
|
+
if (options.autoReturn) {
|
|
631
|
+
params.set("auto_return", "true");
|
|
632
|
+
}
|
|
633
|
+
return `${baseUrl}/?${params.toString()}`;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Redirect in same tab
|
|
637
|
+
*/
|
|
638
|
+
redirect(url) {
|
|
639
|
+
window.location.href = url;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Open in new tab with PostMessage communication and proper cleanup
|
|
643
|
+
*/
|
|
644
|
+
openNewTab(url, sessionId) {
|
|
645
|
+
var _a, _b;
|
|
646
|
+
this.cleanup();
|
|
647
|
+
this.hasReceivedResult = false;
|
|
648
|
+
if (this.popupMonitorInterval) {
|
|
649
|
+
clearInterval(this.popupMonitorInterval);
|
|
650
|
+
this.popupMonitorInterval = null;
|
|
651
|
+
}
|
|
652
|
+
const target = this.config.newTabTarget || "popup";
|
|
653
|
+
if (target === "tab") {
|
|
654
|
+
this.popupWindow = window.open(url, "_blank");
|
|
655
|
+
} else {
|
|
656
|
+
this.popupWindow = window.open(
|
|
657
|
+
url,
|
|
658
|
+
this.brandConstants.popupName,
|
|
659
|
+
"width=600,height=700"
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
if (!this.popupWindow) {
|
|
663
|
+
(_b = (_a = this.config).onError) == null ? void 0 : _b.call(
|
|
664
|
+
_a,
|
|
665
|
+
new Error(
|
|
666
|
+
"Failed to open verification window. Please check popup blocker settings."
|
|
667
|
+
)
|
|
668
|
+
);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const trustedOrigins = this.getTrustedOrigins();
|
|
672
|
+
const allowedCustomOrigins = this.getAllowedCustomOrigins(url);
|
|
673
|
+
const expectedMessageType = this.brandConstants.messageType;
|
|
674
|
+
const legacyMessageType = this.brandConstants.legacyMessageType;
|
|
675
|
+
this.messageListener = (event) => {
|
|
676
|
+
var _a2, _b2, _c, _d, _e, _f, _g;
|
|
677
|
+
const messageType = (_a2 = event.data) == null ? void 0 : _a2.type;
|
|
678
|
+
const allowedTypes = legacyMessageType ? [expectedMessageType, legacyMessageType] : [expectedMessageType];
|
|
679
|
+
if (!messageType || typeof messageType !== "string" || !allowedTypes.includes(messageType)) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if (!validatePostMessageOrigin(event, trustedOrigins, allowedCustomOrigins, this.brandConstants.name)) {
|
|
683
|
+
logSecurityEvent("POSTMESSAGE_ORIGIN_BLOCKED", {
|
|
684
|
+
origin: event.origin,
|
|
685
|
+
environment: this.config.environment,
|
|
686
|
+
expectedOrigins: `${this.brandConstants.name} trusted origins for ${this.config.environment}`,
|
|
687
|
+
messageType: (_b2 = event.data) == null ? void 0 : _b2.type
|
|
688
|
+
}, this.brandConstants.name);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const messageValidation = validateVerificationMessage(
|
|
692
|
+
event,
|
|
693
|
+
sessionId,
|
|
694
|
+
expectedMessageType,
|
|
695
|
+
legacyMessageType
|
|
696
|
+
);
|
|
697
|
+
if (!messageValidation.isValid) {
|
|
698
|
+
logSecurityEvent("POSTMESSAGE_VALIDATION_FAILED", {
|
|
699
|
+
error: messageValidation.error,
|
|
700
|
+
origin: event.origin,
|
|
701
|
+
sessionId: sessionId.substring(0, 8) + "...",
|
|
702
|
+
messageType: (_c = event.data) == null ? void 0 : _c.type
|
|
703
|
+
}, this.brandConstants.name);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const status = event.data.status;
|
|
707
|
+
if (status === "cancelled") {
|
|
708
|
+
this.handleCancellation(sessionId, "postmessage");
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const result = {
|
|
712
|
+
sessionId: event.data.sessionId,
|
|
713
|
+
status,
|
|
714
|
+
timestamp: event.data.timestamp,
|
|
715
|
+
externalUserId: event.data.externalUserId
|
|
716
|
+
};
|
|
717
|
+
this.hasReceivedResult = true;
|
|
718
|
+
logSecurityEvent("VERIFICATION_COMPLETED", {
|
|
719
|
+
status: result.status,
|
|
720
|
+
sessionId: sessionId.substring(0, 8) + "...",
|
|
721
|
+
origin: event.origin
|
|
722
|
+
}, this.brandConstants.name);
|
|
723
|
+
this.cleanup({ closePopup: false });
|
|
724
|
+
this.unlockVerification();
|
|
725
|
+
if (this.popupMonitorInterval) {
|
|
726
|
+
clearInterval(this.popupMonitorInterval);
|
|
727
|
+
this.popupMonitorInterval = null;
|
|
728
|
+
}
|
|
729
|
+
if (result.status === "verified") {
|
|
730
|
+
(_e = (_d = this.config).onComplete) == null ? void 0 : _e.call(_d, result);
|
|
731
|
+
} else {
|
|
732
|
+
(_g = (_f = this.config).onError) == null ? void 0 : _g.call(
|
|
733
|
+
_f,
|
|
734
|
+
new Error(`Verification failed: ${result.status}`)
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
window.addEventListener("message", this.messageListener);
|
|
739
|
+
this.popupMonitorInterval = setInterval(() => {
|
|
740
|
+
if (this.popupWindow && this.popupWindow.closed) {
|
|
741
|
+
logSecurityEvent("POPUP_CLOSED_BY_USER", {
|
|
742
|
+
sessionId: sessionId.substring(0, 8) + "...",
|
|
743
|
+
environment: this.config.environment
|
|
744
|
+
}, this.brandConstants.name);
|
|
745
|
+
if (!this.hasReceivedResult) {
|
|
746
|
+
this.handleCancellation(sessionId, "popup-closed");
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}, 500);
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Set up automatic cleanup on page unload to prevent memory leaks
|
|
753
|
+
*/
|
|
754
|
+
setupAutoCleanup() {
|
|
755
|
+
this.unloadListener = () => {
|
|
756
|
+
logSecurityEvent("SDK_AUTO_CLEANUP", {
|
|
757
|
+
environment: this.config.environment,
|
|
758
|
+
trigger: "page_unload"
|
|
759
|
+
}, this.brandConstants.name);
|
|
760
|
+
this.cleanup();
|
|
761
|
+
this.unlockVerification();
|
|
762
|
+
};
|
|
763
|
+
window.addEventListener("beforeunload", this.unloadListener);
|
|
764
|
+
window.addEventListener("pagehide", this.unloadListener);
|
|
765
|
+
if (window.history && window.history.pushState) {
|
|
766
|
+
const originalPushState = window.history.pushState;
|
|
767
|
+
window.history.pushState = (...args) => {
|
|
768
|
+
this.cleanup();
|
|
769
|
+
this.unlockVerification();
|
|
770
|
+
return originalPushState.apply(window.history, args);
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Auto-detect environment based on current URL
|
|
776
|
+
*/
|
|
777
|
+
detectEnvironment() {
|
|
778
|
+
const hostname = window.location.hostname;
|
|
779
|
+
if (hostname.includes("staging") || hostname.includes("stage")) {
|
|
780
|
+
return "staging";
|
|
781
|
+
}
|
|
782
|
+
return "production";
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Get the current environment
|
|
786
|
+
*/
|
|
787
|
+
getEnvironment() {
|
|
788
|
+
return this.config.environment;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Unlock verification process to allow new verifications
|
|
792
|
+
*/
|
|
793
|
+
unlockVerification() {
|
|
794
|
+
this.isVerificationInProgress = false;
|
|
795
|
+
this.currentSessionId = null;
|
|
796
|
+
logSecurityEvent("VERIFICATION_UNLOCKED", {
|
|
797
|
+
environment: this.config.environment,
|
|
798
|
+
origin: window.location.origin
|
|
799
|
+
}, this.brandConstants.name);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Internal cleanup method to prevent memory leaks
|
|
803
|
+
*/
|
|
804
|
+
cleanup(options = {}) {
|
|
805
|
+
const shouldClosePopup = options.closePopup !== false;
|
|
806
|
+
if (this.popupWindow) {
|
|
807
|
+
if (shouldClosePopup && !this.popupWindow.closed) {
|
|
808
|
+
this.popupWindow.close();
|
|
809
|
+
}
|
|
810
|
+
if (shouldClosePopup || this.popupWindow.closed) {
|
|
811
|
+
this.popupWindow = null;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (this.messageListener) {
|
|
815
|
+
window.removeEventListener("message", this.messageListener);
|
|
816
|
+
this.messageListener = null;
|
|
817
|
+
}
|
|
818
|
+
if (this.popupMonitorInterval) {
|
|
819
|
+
clearInterval(this.popupMonitorInterval);
|
|
820
|
+
this.popupMonitorInterval = null;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
handleCancellation(sessionId, source) {
|
|
824
|
+
if (this.hasReceivedResult) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
this.hasReceivedResult = true;
|
|
828
|
+
logSecurityEvent("VERIFICATION_CANCELLED", {
|
|
829
|
+
source,
|
|
830
|
+
sessionId: sessionId.substring(0, 8) + "...",
|
|
831
|
+
environment: this.config.environment,
|
|
832
|
+
origin: window.location.origin
|
|
833
|
+
}, this.brandConstants.name);
|
|
834
|
+
this.cleanup();
|
|
835
|
+
this.unlockVerification();
|
|
836
|
+
let shouldRedirect = true;
|
|
837
|
+
if (this.config.onCancel) {
|
|
838
|
+
try {
|
|
839
|
+
const result = this.config.onCancel();
|
|
840
|
+
if (result === false) {
|
|
841
|
+
shouldRedirect = false;
|
|
842
|
+
}
|
|
843
|
+
} catch (error) {
|
|
844
|
+
logSecurityEvent("CANCEL_CALLBACK_FAILED", {
|
|
845
|
+
error: error instanceof Error ? error.message : String(error),
|
|
846
|
+
sessionId: sessionId.substring(0, 8) + "..."
|
|
847
|
+
}, this.brandConstants.name);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (shouldRedirect) {
|
|
851
|
+
this.redirectToCancelUrl(sessionId);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
redirectToCancelUrl(sessionId) {
|
|
855
|
+
if (!this.config.cancelUrl) {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
try {
|
|
859
|
+
const decodedUrl = decodeURIComponent(this.config.cancelUrl);
|
|
860
|
+
const redirectUrl = new URL(decodedUrl);
|
|
861
|
+
redirectUrl.searchParams.set("sessionId", sessionId);
|
|
862
|
+
redirectUrl.searchParams.set("status", "cancelled");
|
|
863
|
+
redirectUrl.searchParams.set("timestamp", Date.now().toString());
|
|
864
|
+
if (this.lastExternalUserId) {
|
|
865
|
+
redirectUrl.searchParams.set("externalUserId", this.lastExternalUserId);
|
|
866
|
+
}
|
|
867
|
+
window.location.href = redirectUrl.toString();
|
|
868
|
+
} catch (error) {
|
|
869
|
+
logSecurityEvent("CANCEL_REDIRECT_FAILED", {
|
|
870
|
+
error: error instanceof Error ? error.message : String(error),
|
|
871
|
+
sessionId: sessionId.substring(0, 8) + "..."
|
|
872
|
+
}, this.brandConstants.name);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Remove auto-cleanup listeners
|
|
877
|
+
*/
|
|
878
|
+
removeAutoCleanupListeners() {
|
|
879
|
+
if (this.unloadListener) {
|
|
880
|
+
window.removeEventListener("beforeunload", this.unloadListener);
|
|
881
|
+
window.removeEventListener("pagehide", this.unloadListener);
|
|
882
|
+
this.unloadListener = null;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Public cleanup method for manual resource management
|
|
887
|
+
*/
|
|
888
|
+
destroy() {
|
|
889
|
+
logSecurityEvent("SDK_DESTROYED", {
|
|
890
|
+
environment: this.config.environment,
|
|
891
|
+
origin: window.location.origin
|
|
892
|
+
}, this.brandConstants.name);
|
|
893
|
+
this.cleanup();
|
|
894
|
+
this.unlockVerification();
|
|
895
|
+
this.removeAutoCleanupListeners();
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Get Portal API URL based on environment and brand
|
|
899
|
+
*/
|
|
900
|
+
getPortalApiUrl() {
|
|
901
|
+
if (this.config.apiUrl) {
|
|
902
|
+
return this.config.apiUrl;
|
|
903
|
+
}
|
|
904
|
+
return this.getUrlConfig().apiUrl;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Get Engine URL based on environment and brand
|
|
908
|
+
*/
|
|
909
|
+
getEngineUrl() {
|
|
910
|
+
return this.getUrlConfig().engineUrl;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Get WebSocket URL based on environment and brand
|
|
914
|
+
*/
|
|
915
|
+
getWebSocketUrl() {
|
|
916
|
+
return this.getUrlConfig().wsUrl;
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Detect if this is a public key (pk_ prefix) vs private key (sk_ prefix)
|
|
920
|
+
*/
|
|
921
|
+
isPublicKey() {
|
|
922
|
+
return this.config.apiKey.startsWith("pk_");
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Create session internally for public keys
|
|
926
|
+
*/
|
|
927
|
+
async createInternalSession(options) {
|
|
928
|
+
var _a, _b;
|
|
929
|
+
try {
|
|
930
|
+
const portalApiUrl = this.getPortalApiUrl();
|
|
931
|
+
const response = await fetch(`${portalApiUrl}/api/v1/sessions/create`, {
|
|
932
|
+
method: "POST",
|
|
933
|
+
headers: {
|
|
934
|
+
"Content-Type": "application/json",
|
|
935
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
936
|
+
},
|
|
937
|
+
body: JSON.stringify({
|
|
938
|
+
merchantId: this.config.apiKey,
|
|
939
|
+
returnUrl: this.config.returnUrl,
|
|
940
|
+
cancelUrl: this.config.cancelUrl,
|
|
941
|
+
challengeAge: options.challengeAge,
|
|
942
|
+
verificationMode: options.verificationMode,
|
|
943
|
+
merchantName: document.title || window.location.hostname,
|
|
944
|
+
externalUserId: options.externalUserId
|
|
945
|
+
})
|
|
946
|
+
});
|
|
947
|
+
if (!response.ok) {
|
|
948
|
+
const errorData = await response.json().catch(() => ({}));
|
|
949
|
+
throw new Error(
|
|
950
|
+
`Failed to create session: ${response.status} ${response.statusText}. ${errorData.message || ""}`
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
const sessionData = await response.json();
|
|
954
|
+
const sessionId = sessionData.sessionId;
|
|
955
|
+
if (!sessionId) {
|
|
956
|
+
throw new Error("Server did not return a sessionId");
|
|
957
|
+
}
|
|
958
|
+
if (sessionData.verifyUrl) {
|
|
959
|
+
this.lastVerifyUrl = sessionData.verifyUrl;
|
|
960
|
+
}
|
|
961
|
+
if (sessionData.sessionToken) {
|
|
962
|
+
this.lastSessionToken = sessionData.sessionToken;
|
|
963
|
+
}
|
|
964
|
+
if (sessionData.handoffToken) {
|
|
965
|
+
this.temporaryHandoffToken = sessionData.handoffToken;
|
|
966
|
+
}
|
|
967
|
+
logSecurityEvent("INTERNAL_SESSION_CREATED", {
|
|
968
|
+
sessionId: sessionId.substring(0, 8) + "...",
|
|
969
|
+
environment: this.config.environment,
|
|
970
|
+
apiKeyType: "public"
|
|
971
|
+
}, this.brandConstants.name);
|
|
972
|
+
return sessionId;
|
|
973
|
+
} catch (error) {
|
|
974
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
975
|
+
logSecurityEvent("INTERNAL_SESSION_FAILED", {
|
|
976
|
+
error: errorMessage,
|
|
977
|
+
environment: this.config.environment,
|
|
978
|
+
apiKeyType: "public"
|
|
979
|
+
}, this.brandConstants.name);
|
|
980
|
+
(_b = (_a = this.config).onError) == null ? void 0 : _b.call(_a, error);
|
|
981
|
+
throw new Error(`Failed to create verification session: ${errorMessage}`);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
getUrlConfig() {
|
|
985
|
+
return this.brandUrls[this.config.environment] || this.brandUrls.production;
|
|
986
|
+
}
|
|
987
|
+
getTrustedOrigins() {
|
|
988
|
+
return this.getUrlConfig().trustedOrigins;
|
|
989
|
+
}
|
|
990
|
+
getAllowedCustomOrigins(verificationUrl) {
|
|
991
|
+
const origins = /* @__PURE__ */ new Set();
|
|
992
|
+
const overrideOrigin = this.getLocalOrigin(this.config.verifyUrl || null);
|
|
993
|
+
const verificationOrigin = this.getLocalOrigin(verificationUrl || null);
|
|
994
|
+
if (overrideOrigin) {
|
|
995
|
+
origins.add(overrideOrigin);
|
|
996
|
+
}
|
|
997
|
+
if (verificationOrigin) {
|
|
998
|
+
origins.add(verificationOrigin);
|
|
999
|
+
}
|
|
1000
|
+
return Array.from(origins);
|
|
1001
|
+
}
|
|
1002
|
+
getLocalOrigin(urlValue) {
|
|
1003
|
+
if (!urlValue) {
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
try {
|
|
1007
|
+
const parsed = new URL(urlValue);
|
|
1008
|
+
if (_VerificationSDK.LOCAL_HOSTNAMES.has(parsed.hostname)) {
|
|
1009
|
+
return parsed.origin;
|
|
1010
|
+
}
|
|
1011
|
+
} catch (e) {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
applyLocalVerifyOverride(rawUrl) {
|
|
1017
|
+
const overrideOrigin = this.getLocalOrigin(this.config.verifyUrl || null);
|
|
1018
|
+
if (!overrideOrigin) {
|
|
1019
|
+
return rawUrl;
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
const overrideUrl = new URL(overrideOrigin);
|
|
1023
|
+
const targetUrl = new URL(rawUrl);
|
|
1024
|
+
targetUrl.protocol = overrideUrl.protocol;
|
|
1025
|
+
targetUrl.host = overrideUrl.host;
|
|
1026
|
+
return targetUrl.toString();
|
|
1027
|
+
} catch (e) {
|
|
1028
|
+
return rawUrl;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
getHmacSecret() {
|
|
1032
|
+
return this.config.environment === "staging" ? this.brandConstants.hmacSecretStaging : this.brandConstants.hmacSecretProd;
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
// Local override hostnames allowed for internal testing
|
|
1036
|
+
_VerificationSDK.LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
1037
|
+
var VerificationSDK = _VerificationSDK;
|
|
1038
|
+
|
|
1039
|
+
// src-redirect/brands/safepassage/urls.ts
|
|
1040
|
+
var BRAND_URLS = {
|
|
1041
|
+
production: {
|
|
1042
|
+
apiUrl: "https://api.safepassage.live",
|
|
1043
|
+
verifyUiUrl: "https://av.safepassage.live",
|
|
1044
|
+
engineUrl: "https://engine.safepassage.live",
|
|
1045
|
+
wsUrl: "wss://engine.safepassage.live/api/websocket/stream",
|
|
1046
|
+
trustedOrigins: [
|
|
1047
|
+
"https://av.safepassage.live",
|
|
1048
|
+
"https://portal.safepassage.live",
|
|
1049
|
+
"https://api.safepassage.live"
|
|
1050
|
+
]
|
|
1051
|
+
},
|
|
1052
|
+
staging: {
|
|
1053
|
+
apiUrl: "https://api.staging.safepassage.live",
|
|
1054
|
+
verifyUiUrl: "https://av.staging.safepassage.live",
|
|
1055
|
+
engineUrl: "https://engine.staging.safepassage.live",
|
|
1056
|
+
wsUrl: "wss://engine.staging.safepassage.live/api/websocket/stream",
|
|
1057
|
+
trustedOrigins: [
|
|
1058
|
+
"https://av.staging.safepassage.live",
|
|
1059
|
+
"https://portal.staging.safepassage.live",
|
|
1060
|
+
"https://api.staging.safepassage.live"
|
|
1061
|
+
]
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
var BRAND_CONSTANTS = {
|
|
1065
|
+
name: "SafePassage",
|
|
1066
|
+
hmacSecretProd: "safepassage-prod-hmac-2025",
|
|
1067
|
+
hmacSecretStaging: "safepassage-stage-hmac-2025",
|
|
1068
|
+
messageType: "safepassage:verification:complete",
|
|
1069
|
+
legacyMessageType: "safepassage-verification",
|
|
1070
|
+
popupName: "safepassage-verify",
|
|
1071
|
+
docsUrl: "https://docs.safepassage.live"
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
// src-redirect/brands/safepassage/index.ts
|
|
1075
|
+
var SafePassage = class extends VerificationSDK {
|
|
1076
|
+
constructor(config) {
|
|
1077
|
+
super(config, BRAND_URLS, BRAND_CONSTANTS);
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
var VERSION = "3.4.9";
|
|
1081
|
+
SafePassage.VERSION = VERSION;
|
|
1082
|
+
if (typeof window !== "undefined") {
|
|
1083
|
+
setupPolyfills();
|
|
1084
|
+
checkBrowserCompatibility(`${BRAND_CONSTANTS.name} SDK`);
|
|
1085
|
+
}
|
|
1086
|
+
var index_default = SafePassage;
|
|
1087
|
+
export {
|
|
1088
|
+
SafePassage,
|
|
1089
|
+
VERSION,
|
|
1090
|
+
index_default as default
|
|
1091
|
+
};
|