@safepassage/sdk 3.4.1 → 3.4.3

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 CHANGED
@@ -1,40 +1,1084 @@
1
- /**
2
- * SafePassage SDK - Redirect-based age verification
3
- *
4
- * @example
5
- * ```javascript
6
- * // Initialize SDK
7
- * const sp = new SafePassage({
8
- * apiKey: 'pk_xxxxx',
9
- * returnUrl: 'https://merchant.com/verified',
10
- * cancelUrl: 'https://merchant.com/cancelled'
11
- * });
12
- *
13
- * // Trigger verification
14
- * sp.verify({
15
- * sessionId: generateUUID() // Merchant-generated UUID v4
16
- * });
17
- * ```
18
- */
19
- // Setup polyfills and check browser compatibility
20
- import { setupPolyfills, checkBrowserCompatibility } from './utils/polyfills';
21
- // Initialize polyfills immediately
22
- if (typeof window !== 'undefined') {
23
- setupPolyfills();
24
- checkBrowserCompatibility();
25
- }
26
- export { SafePassage, SafePassage as default } from './core/SafePassageSDK';
27
- // Version - 3.4.1: Rebuilt minified bundle with cancelUrl deprecation fix
28
- export const VERSION = '3.4.1';
29
- // For UMD builds
30
- if (typeof window !== 'undefined' && window) {
31
- // Dynamic import for UMD builds
32
- // eslint-disable-next-line @typescript-eslint/no-require-imports
33
- const { SafePassage } = require('./core/SafePassageSDK');
34
- // Attach to window object for global access
35
- const globalWindow = window;
36
- globalWindow.SafePassage = SafePassage;
37
- if (globalWindow.SafePassage) {
38
- globalWindow.SafePassage.VERSION = VERSION;
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]);
39
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
+ // Server-provided verify URL (includes sessionToken)
479
+ this.lastVerifyUrl = null;
480
+ // Server-provided session token (WS auth)
481
+ this.lastSessionToken = null;
482
+ // Temporary storage for QR handoff token to include in state
483
+ this.temporaryHandoffToken = null;
484
+ validateConfig(config);
485
+ let normalizedEnvironment = config.environment || this.detectEnvironment();
486
+ if (normalizedEnvironment !== "staging" && normalizedEnvironment !== "production") {
487
+ console.warn(`SafePassage SDK: Unknown environment '${normalizedEnvironment}', defaulting to 'production'`);
488
+ normalizedEnvironment = "production";
489
+ }
490
+ this.config = __spreadProps(__spreadValues({}, config), {
491
+ environment: normalizedEnvironment,
492
+ mode: config.mode || "redirect"
493
+ });
494
+ validateEnvironmentSecurity(this.config.environment);
495
+ enforceHTTPS(this.config.environment);
496
+ logSecurityEvent("SDK_INITIALIZED", {
497
+ environment: this.config.environment,
498
+ mode: this.config.mode,
499
+ origin: window.location.origin,
500
+ protocol: window.location.protocol,
501
+ hostname: window.location.hostname
502
+ });
503
+ this.setupAutoCleanup();
504
+ }
505
+ /**
506
+ * Initiate age verification with race condition protection
507
+ *
508
+ * Main verification method that handles session creation, security validation,
509
+ * and verification flow initiation. Includes race condition protection and
510
+ * comprehensive error handling.
511
+ *
512
+ * For public keys (pk_*), automatically creates sessions via the portal API.
513
+ * For private keys (sk_*), requires a pre-created sessionId.
514
+ *
515
+ * @param {VerificationOptions} [options={}] - Verification options
516
+ * @param {string} [options.sessionId] - Session ID (required for private keys)
517
+ * @param {number} [options.challengeAge] - Age challenge override
518
+ * @param {string} [options.verificationMode] - Verification mode override
519
+ * @param {string} [options.externalUserId] - External user identifier
520
+ * @returns {Promise<void>} Promise that resolves when verification is initiated
521
+ * @throws {Error} If verification cannot be started or is already in progress
522
+ */
523
+ async verify(options = {}) {
524
+ var _a, _b, _c, _d, _e, _f;
525
+ const isPublicKey = this.isPublicKey();
526
+ let sessionId;
527
+ if (isPublicKey) {
528
+ sessionId = await this.createInternalSession(options);
529
+ } else {
530
+ throw new Error(
531
+ "Private API keys (sk_) should use the direct API, not the SDK. The SDK is designed for browser-based public key usage only."
532
+ );
533
+ }
534
+ if (!sessionId) {
535
+ throw new Error("Failed to obtain sessionId from server");
536
+ }
537
+ if (this.isVerificationInProgress) {
538
+ const error = new Error(
539
+ `Verification already in progress for session ${(_a = this.currentSessionId) == null ? void 0 : _a.substring(0, 8)}...`
540
+ );
541
+ logSecurityEvent("RACE_CONDITION_PREVENTED", {
542
+ currentSession: ((_b = this.currentSessionId) == null ? void 0 : _b.substring(0, 8)) + "...",
543
+ attemptedSession: "new-session-attempt",
544
+ origin: window.location.origin
545
+ });
546
+ (_d = (_c = this.config).onError) == null ? void 0 : _d.call(_c, error);
547
+ throw error;
548
+ }
549
+ this.isVerificationInProgress = true;
550
+ this.currentSessionId = sessionId;
551
+ try {
552
+ const rateLimitKey = `${this.config.apiKey}:${window.location.origin}`;
553
+ if (!verificationRateLimit.isAllowed(rateLimitKey)) {
554
+ const error = new Error(
555
+ "Too many verification attempts. Please wait before trying again."
556
+ );
557
+ logSecurityEvent("RATE_LIMIT_EXCEEDED", {
558
+ apiKey: this.config.apiKey.substring(0, 8) + "...",
559
+ origin: window.location.origin,
560
+ sessionId: sessionId ? sessionId.substring(0, 8) + "..." : "undefined"
561
+ });
562
+ (_f = (_e = this.config).onError) == null ? void 0 : _f.call(_e, error);
563
+ throw error;
564
+ }
565
+ const verificationUrl = await this.buildVerificationUrl(
566
+ options,
567
+ sessionId
568
+ );
569
+ logSecurityEvent("VERIFICATION_INITIATED", {
570
+ environment: this.config.environment,
571
+ mode: this.config.mode,
572
+ sessionId: sessionId ? sessionId.substring(0, 8) + "..." : "undefined",
573
+ origin: window.location.origin
574
+ });
575
+ if (this.config.mode === "new-tab") {
576
+ this.openNewTab(verificationUrl, sessionId);
577
+ } else {
578
+ this.unlockVerification();
579
+ this.redirect(verificationUrl);
580
+ }
581
+ } catch (error) {
582
+ this.unlockVerification();
583
+ throw error;
584
+ }
585
+ }
586
+ /**
587
+ * Build verification URL with HMAC-signed state
588
+ *
589
+ * Constructs the verification URL with signed state parameter containing
590
+ * all necessary configuration and security information. The state parameter
591
+ * includes HMAC signature for integrity protection.
592
+ *
593
+ * @param {VerificationOptions} options - Verification options
594
+ * @returns {Promise<string>} Complete verification URL
595
+ * @private
596
+ */
597
+ async buildVerificationUrl(options, sessionId) {
598
+ const baseUrl = getEnvironmentUrl(this.config.environment);
599
+ const hasExplicitChallengeAge = options.challengeAge !== void 0;
600
+ const hasExplicitVerificationMode = options.verificationMode !== void 0;
601
+ const hasOverrides = hasExplicitChallengeAge || hasExplicitVerificationMode;
602
+ const state = await generateState(
603
+ {
604
+ merchantId: this.config.apiKey,
605
+ sessionId,
606
+ returnUrl: this.config.returnUrl,
607
+ cancelUrl: this.config.cancelUrl,
608
+ challengeAge: options.challengeAge || this.config.defaultChallengeAge,
609
+ verificationMode: options.verificationMode || this.config.defaultVerificationMode,
610
+ hasOverrides,
611
+ // Flag to indicate explicit overrides
612
+ externalUserId: options.externalUserId,
613
+ timestamp: Date.now(),
614
+ // Phase 2: Include config for self-contained verify-ui
615
+ apiUrl: this.getPortalApiUrl(),
616
+ engineUrl: this.getEngineUrl(),
617
+ wsUrl: this.getWebSocketUrl(),
618
+ environment: this.config.environment,
619
+ features: {
620
+ testMode: false,
621
+ warmupPeriodMs: 500,
622
+ qualityThreshold: 0.6
623
+ },
624
+ // Include handoffToken if available (for QR code desktop flow)
625
+ handoffToken: this.temporaryHandoffToken || void 0,
626
+ // Include sessionToken and verifyUrl to make UI auth deterministic
627
+ sessionToken: this.lastSessionToken || void 0,
628
+ verifyUrl: this.lastVerifyUrl || void 0
629
+ },
630
+ this.config.environment
631
+ );
632
+ if (this.lastVerifyUrl) {
633
+ try {
634
+ const url = new URL(this.lastVerifyUrl);
635
+ url.searchParams.set("state", state);
636
+ url.searchParams.set("mode", this.config.mode);
637
+ if (options.skipIntro) {
638
+ url.searchParams.set("skip_intro", "true");
639
+ }
640
+ if (options.autoReturn) {
641
+ url.searchParams.set("auto_return", "true");
642
+ }
643
+ return url.toString();
644
+ } catch (e) {
645
+ }
646
+ }
647
+ const params = new URLSearchParams({ state, sessionId, mode: this.config.mode });
648
+ if (options.skipIntro) {
649
+ params.set("skip_intro", "true");
650
+ }
651
+ if (options.autoReturn) {
652
+ params.set("auto_return", "true");
653
+ }
654
+ return `${baseUrl}/?${params.toString()}`;
655
+ }
656
+ /**
657
+ * Redirect in same tab
658
+ *
659
+ * Performs a full page redirect to the verification URL.
660
+ * Used for redirect mode verification.
661
+ *
662
+ * @param {string} url - Verification URL to redirect to
663
+ * @private
664
+ */
665
+ redirect(url) {
666
+ window.location.href = url;
667
+ }
668
+ /**
669
+ * Open in new tab with PostMessage communication and proper cleanup
670
+ *
671
+ * Opens verification URL in a new tab/window and sets up secure PostMessage
672
+ * communication for receiving verification results. Includes comprehensive
673
+ * security validation and automatic cleanup.
674
+ *
675
+ * @param {string} url - Verification URL to open
676
+ * @param {string} sessionId - Session ID for result correlation
677
+ * @private
678
+ */
679
+ openNewTab(url, sessionId) {
680
+ var _a, _b;
681
+ this.cleanup();
682
+ if (this.popupMonitorInterval) {
683
+ clearInterval(this.popupMonitorInterval);
684
+ this.popupMonitorInterval = null;
685
+ }
686
+ this.popupWindow = window.open(
687
+ url,
688
+ "safepassage-verify",
689
+ "width=600,height=700"
690
+ );
691
+ if (!this.popupWindow) {
692
+ (_b = (_a = this.config).onError) == null ? void 0 : _b.call(
693
+ _a,
694
+ new Error(
695
+ "Failed to open verification window. Please check popup blocker settings."
696
+ )
697
+ );
698
+ return;
699
+ }
700
+ this.messageListener = (event) => {
701
+ var _a2, _b2, _c, _d, _e, _f;
702
+ if (!validatePostMessageOrigin(event, this.config.environment)) {
703
+ logSecurityEvent("POSTMESSAGE_ORIGIN_BLOCKED", {
704
+ origin: event.origin,
705
+ environment: this.config.environment,
706
+ expectedOrigins: `SafePassage trusted origins for ${this.config.environment}`,
707
+ messageType: (_a2 = event.data) == null ? void 0 : _a2.type
708
+ });
709
+ return;
710
+ }
711
+ const messageValidation = validateSafePassageMessage(event, sessionId);
712
+ if (!messageValidation.isValid) {
713
+ logSecurityEvent("POSTMESSAGE_VALIDATION_FAILED", {
714
+ error: messageValidation.error,
715
+ origin: event.origin,
716
+ sessionId: sessionId.substring(0, 8) + "...",
717
+ messageType: (_b2 = event.data) == null ? void 0 : _b2.type
718
+ });
719
+ return;
720
+ }
721
+ const result = {
722
+ sessionId: event.data.sessionId,
723
+ status: event.data.status
724
+ };
725
+ logSecurityEvent("VERIFICATION_COMPLETED", {
726
+ status: result.status,
727
+ sessionId: sessionId.substring(0, 8) + "...",
728
+ origin: event.origin
729
+ });
730
+ this.cleanup();
731
+ this.unlockVerification();
732
+ if (this.popupMonitorInterval) {
733
+ clearInterval(this.popupMonitorInterval);
734
+ this.popupMonitorInterval = null;
735
+ }
736
+ if (result.status === "verified") {
737
+ (_d = (_c = this.config).onComplete) == null ? void 0 : _d.call(_c, result);
738
+ } else {
739
+ (_f = (_e = this.config).onError) == null ? void 0 : _f.call(
740
+ _e,
741
+ new Error(`Verification failed: ${result.status}`)
742
+ );
743
+ }
744
+ };
745
+ window.addEventListener("message", this.messageListener);
746
+ this.popupMonitorInterval = setInterval(() => {
747
+ var _a2, _b2;
748
+ if (this.popupWindow && this.popupWindow.closed) {
749
+ logSecurityEvent("POPUP_CLOSED_BY_USER", {
750
+ sessionId: sessionId.substring(0, 8) + "...",
751
+ environment: this.config.environment
752
+ });
753
+ this.cleanup();
754
+ this.unlockVerification();
755
+ (_b2 = (_a2 = this.config).onCancel) == null ? void 0 : _b2.call(_a2);
756
+ }
757
+ }, 500);
758
+ }
759
+ /**
760
+ * Set up automatic cleanup on page unload to prevent memory leaks
761
+ *
762
+ * Registers event listeners for page unload events to ensure proper
763
+ * cleanup of resources and verification state. Handles both traditional
764
+ * page navigation and single-page application route changes.
765
+ *
766
+ * @private
767
+ */
768
+ setupAutoCleanup() {
769
+ this.unloadListener = () => {
770
+ logSecurityEvent("SDK_AUTO_CLEANUP", {
771
+ environment: this.config.environment,
772
+ trigger: "page_unload"
773
+ });
774
+ this.cleanup();
775
+ this.unlockVerification();
776
+ };
777
+ window.addEventListener("beforeunload", this.unloadListener);
778
+ window.addEventListener("pagehide", this.unloadListener);
779
+ if (window.history && window.history.pushState) {
780
+ const originalPushState = window.history.pushState;
781
+ window.history.pushState = (...args) => {
782
+ this.cleanup();
783
+ this.unlockVerification();
784
+ return originalPushState.apply(window.history, args);
785
+ };
786
+ }
787
+ }
788
+ /**
789
+ * Auto-detect environment based on current URL
790
+ *
791
+ * Analyzes the current hostname to determine the appropriate environment
792
+ * configuration. Used when environment is not explicitly specified.
793
+ * Always defaults to production unless staging is detected.
794
+ *
795
+ * @returns {'production' | 'staging'} Detected environment
796
+ * @private
797
+ */
798
+ detectEnvironment() {
799
+ const hostname = window.location.hostname;
800
+ if (hostname.includes("staging") || hostname.includes("stage")) {
801
+ return "staging";
802
+ }
803
+ return "production";
804
+ }
805
+ /**
806
+ * Get the current environment
807
+ * @returns {string} The current environment (production or staging)
808
+ */
809
+ getEnvironment() {
810
+ return this.config.environment;
811
+ }
812
+ /**
813
+ * Unlock verification process to allow new verifications
814
+ *
815
+ * Resets the verification lock state to allow new verification attempts.
816
+ * Called after successful completion, errors, or cleanup.
817
+ *
818
+ * @private
819
+ */
820
+ unlockVerification() {
821
+ this.isVerificationInProgress = false;
822
+ this.currentSessionId = null;
823
+ logSecurityEvent("VERIFICATION_UNLOCKED", {
824
+ environment: this.config.environment,
825
+ origin: window.location.origin
826
+ });
827
+ }
828
+ /**
829
+ * Internal cleanup method to prevent memory leaks
830
+ *
831
+ * Cleans up popup windows, event listeners, and intervals.
832
+ * Does not unlock verification state - that's handled by specific callers.
833
+ *
834
+ * @private
835
+ */
836
+ cleanup() {
837
+ if (this.popupWindow && !this.popupWindow.closed) {
838
+ this.popupWindow.close();
839
+ }
840
+ this.popupWindow = null;
841
+ if (this.messageListener) {
842
+ window.removeEventListener("message", this.messageListener);
843
+ this.messageListener = null;
844
+ }
845
+ if (this.popupMonitorInterval) {
846
+ clearInterval(this.popupMonitorInterval);
847
+ this.popupMonitorInterval = null;
848
+ }
849
+ }
850
+ /**
851
+ * Remove auto-cleanup listeners
852
+ *
853
+ * Removes page unload event listeners that were set up for automatic cleanup.
854
+ *
855
+ * @private
856
+ */
857
+ removeAutoCleanupListeners() {
858
+ if (this.unloadListener) {
859
+ window.removeEventListener("beforeunload", this.unloadListener);
860
+ window.removeEventListener("pagehide", this.unloadListener);
861
+ this.unloadListener = null;
862
+ }
863
+ }
864
+ /**
865
+ * Public cleanup method for manual resource management
866
+ *
867
+ * Completely destroys the SDK instance, cleaning up all resources and
868
+ * removing all event listeners. Should be called when the SDK is no longer needed.
869
+ *
870
+ * @public
871
+ */
872
+ destroy() {
873
+ logSecurityEvent("SDK_DESTROYED", {
874
+ environment: this.config.environment,
875
+ origin: window.location.origin
876
+ });
877
+ this.cleanup();
878
+ this.unlockVerification();
879
+ this.removeAutoCleanupListeners();
880
+ }
881
+ /**
882
+ * Get Portal API URL based on environment
883
+ *
884
+ * Returns the appropriate portal-api URL for the current environment.
885
+ *
886
+ * @returns {string} Portal API URL
887
+ * @private
888
+ */
889
+ getPortalApiUrl() {
890
+ switch (this.config.environment) {
891
+ case "staging":
892
+ return "https://api.staging.safepassageapp.com";
893
+ case "production":
894
+ return "https://api.safepassageapp.com";
895
+ default:
896
+ return "https://api.safepassageapp.com";
897
+ }
898
+ }
899
+ /**
900
+ * Get Engine URL based on environment
901
+ *
902
+ * Returns the appropriate verify-engine URL for the current environment.
903
+ * In production, engine access is proxied through portal-api.
904
+ *
905
+ * @returns {string} Engine URL
906
+ * @private
907
+ */
908
+ getEngineUrl() {
909
+ switch (this.config.environment) {
910
+ case "staging":
911
+ return "https://engine.staging.safepassageapp.com";
912
+ case "production":
913
+ return "https://engine.safepassageapp.com";
914
+ default:
915
+ return "https://engine.safepassageapp.com";
916
+ }
917
+ }
918
+ /**
919
+ * Get WebSocket URL based on environment
920
+ *
921
+ * Returns the appropriate WebSocket URL for the current environment.
922
+ * In production, WebSocket connections are proxied through portal-api.
923
+ *
924
+ * @returns {string} WebSocket URL
925
+ * @private
926
+ */
927
+ getWebSocketUrl() {
928
+ switch (this.config.environment) {
929
+ case "staging":
930
+ return "wss://engine.staging.safepassageapp.com/api/websocket/stream";
931
+ case "production":
932
+ return "wss://engine.safepassageapp.com/api/websocket/stream";
933
+ default:
934
+ return "wss://engine.safepassageapp.com/api/websocket/stream";
935
+ }
936
+ }
937
+ /**
938
+ * Detect if this is a public key (pk_ prefix) vs private key (sk_ prefix)
939
+ *
940
+ * Determines API key type based on prefix to handle different authentication flows.
941
+ *
942
+ * @returns {boolean} True if public key, false if private key
943
+ * @private
944
+ */
945
+ isPublicKey() {
946
+ return this.config.apiKey.startsWith("pk_");
947
+ }
948
+ /**
949
+ * Create session internally for public keys
950
+ *
951
+ * Creates a verification session via the portal API for public key authentication.
952
+ * Generates a UUID session ID and submits session creation request with
953
+ * verification parameters.
954
+ *
955
+ * @param {VerificationOptions} options - Verification options
956
+ * @returns {Promise<string>} Created session ID
957
+ * @throws {Error} If session creation fails
958
+ * @private
959
+ */
960
+ async createInternalSession(options) {
961
+ var _a, _b;
962
+ try {
963
+ const portalApiUrl = this.getPortalApiUrl();
964
+ const response = await fetch(`${portalApiUrl}/api/v1/sessions/create`, {
965
+ method: "POST",
966
+ headers: {
967
+ "Content-Type": "application/json",
968
+ Authorization: `Bearer ${this.config.apiKey}`
969
+ },
970
+ body: JSON.stringify({
971
+ merchantId: this.config.apiKey,
972
+ // API expects merchantId even though it uses auth header
973
+ returnUrl: this.config.returnUrl,
974
+ cancelUrl: this.config.cancelUrl,
975
+ challengeAge: options.challengeAge,
976
+ verificationMode: options.verificationMode,
977
+ merchantName: document.title || window.location.hostname,
978
+ externalUserId: options.externalUserId
979
+ })
980
+ });
981
+ if (!response.ok) {
982
+ const errorData = await response.json().catch(() => ({}));
983
+ throw new Error(
984
+ `Failed to create session: ${response.status} ${response.statusText}. ${errorData.message || ""}`
985
+ );
986
+ }
987
+ const sessionData = await response.json();
988
+ const sessionId = sessionData.sessionId;
989
+ if (!sessionId) {
990
+ throw new Error("Server did not return a sessionId");
991
+ }
992
+ if (sessionData.verifyUrl) {
993
+ this.lastVerifyUrl = sessionData.verifyUrl;
994
+ }
995
+ if (sessionData.sessionToken) {
996
+ this.lastSessionToken = sessionData.sessionToken;
997
+ }
998
+ if (sessionData.handoffToken) {
999
+ this.temporaryHandoffToken = sessionData.handoffToken;
1000
+ }
1001
+ logSecurityEvent("INTERNAL_SESSION_CREATED", {
1002
+ sessionId: sessionId.substring(0, 8) + "...",
1003
+ environment: this.config.environment,
1004
+ apiKeyType: "public"
1005
+ });
1006
+ return sessionId;
1007
+ } catch (error) {
1008
+ const errorMessage = error instanceof Error ? error.message : String(error);
1009
+ logSecurityEvent("INTERNAL_SESSION_FAILED", {
1010
+ error: errorMessage,
1011
+ environment: this.config.environment,
1012
+ apiKeyType: "public"
1013
+ });
1014
+ (_b = (_a = this.config).onError) == null ? void 0 : _b.call(_a, error);
1015
+ throw new Error(`Failed to create verification session: ${errorMessage}`);
1016
+ }
1017
+ }
1018
+ };
1019
+ SafePassageSDK_default = SafePassage;
1020
+ }
1021
+ });
1022
+
1023
+ // src-redirect/utils/polyfills.ts
1024
+ function setupPolyfills() {
1025
+ if (!crypto.randomUUID) {
1026
+ crypto.randomUUID = function() {
1027
+ const array = new Uint8Array(16);
1028
+ crypto.getRandomValues(array);
1029
+ array[6] = array[6] & 15 | 64;
1030
+ array[8] = array[8] & 63 | 128;
1031
+ const hex = Array.from(array).map((b) => b.toString(16).padStart(2, "0")).join("");
1032
+ return [
1033
+ hex.slice(0, 8),
1034
+ hex.slice(8, 12),
1035
+ hex.slice(12, 16),
1036
+ hex.slice(16, 20),
1037
+ hex.slice(20, 32)
1038
+ ].join("-");
1039
+ };
1040
+ }
1041
+ }
1042
+ function checkBrowserCompatibility() {
1043
+ const warnings = [];
1044
+ if (!window.crypto || !window.crypto.getRandomValues) {
1045
+ throw new Error("SafePassage SDK requires Web Crypto API support");
1046
+ }
1047
+ if (!window.crypto.subtle) {
1048
+ throw new Error(
1049
+ "SafePassage SDK requires Web Crypto subtle API for HMAC operations"
1050
+ );
1051
+ }
1052
+ if (!crypto.randomUUID) {
1053
+ warnings.push("crypto.randomUUID not supported, using polyfill");
1054
+ }
1055
+ if (!window.URLSearchParams) {
1056
+ warnings.push(
1057
+ "URLSearchParams not supported, consider adding a polyfill for IE 11 support"
1058
+ );
1059
+ }
1060
+ if (warnings.length > 0) {
1061
+ console.warn("SafePassage SDK Browser Compatibility:", warnings.join("; "));
1062
+ }
1063
+ }
1064
+
1065
+ // src-redirect/index.ts
1066
+ init_SafePassageSDK();
1067
+ if (typeof window !== "undefined") {
1068
+ setupPolyfills();
1069
+ checkBrowserCompatibility();
1070
+ }
1071
+ var VERSION = "3.4.3";
1072
+ if (typeof window !== "undefined" && window) {
1073
+ const { SafePassage: SafePassage2 } = (init_SafePassageSDK(), __toCommonJS(SafePassageSDK_exports));
1074
+ const globalWindow = window;
1075
+ globalWindow.SafePassage = SafePassage2;
1076
+ if (globalWindow.SafePassage) {
1077
+ globalWindow.SafePassage.VERSION = VERSION;
1078
+ }
40
1079
  }
1080
+ export {
1081
+ SafePassage,
1082
+ VERSION,
1083
+ SafePassage as default
1084
+ };