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