@safepassage/sdk 3.4.8 → 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/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
+ };