@safepassage/sdk 3.4.2 → 3.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -178,6 +178,8 @@ For reliable verification tracking, configure webhooks in your dashboard:
178
178
  }
179
179
  ```
180
180
 
181
+ Webhook `timestamp` values are ISO 8601 strings. SDK `onComplete` results use milliseconds since epoch.
182
+
181
183
  ## Complete Example
182
184
 
183
185
  ### HTML + CDN
@@ -246,6 +248,8 @@ import { SafePassage, SafePassageConfig, VerificationResult } from '@safepassage
246
248
  const config: SafePassageConfig = {
247
249
  apiKey: process.env.NEXT_PUBLIC_SAFEPASSAGE_KEY!,
248
250
  returnUrl: '/verified',
251
+ // Optional: used as a fallback redirect for Emblem login failures
252
+ cancelUrl: '/verify-cancelled',
249
253
  mode: 'new-tab',
250
254
  onComplete: (result: VerificationResult) => {
251
255
  console.log(`Session ${result.sessionId}: ${result.status}`);
@@ -49,6 +49,7 @@ export declare class SafePassage {
49
49
  private currentSessionId;
50
50
  private lastVerifyUrl;
51
51
  private lastSessionToken;
52
+ private temporaryHandoffToken;
52
53
  /**
53
54
  * Initialize SafePassage SDK
54
55
  *
@@ -63,6 +63,8 @@ export class SafePassage {
63
63
  this.lastVerifyUrl = null;
64
64
  // Server-provided session token (WS auth)
65
65
  this.lastSessionToken = null;
66
+ // Temporary storage for QR handoff token to include in state
67
+ this.temporaryHandoffToken = null;
66
68
  validateConfig(config);
67
69
  // Normalize environment: treat 'development' or any unknown value as 'production'
68
70
  let normalizedEnvironment = config.environment || this.detectEnvironment();
@@ -210,7 +212,7 @@ export class SafePassage {
210
212
  qualityThreshold: 0.6,
211
213
  },
212
214
  // Include handoffToken if available (for QR code desktop flow)
213
- handoffToken: this._temporaryHandoffToken,
215
+ handoffToken: this.temporaryHandoffToken || undefined,
214
216
  // Include sessionToken and verifyUrl to make UI auth deterministic
215
217
  sessionToken: this.lastSessionToken || undefined,
216
218
  verifyUrl: this.lastVerifyUrl || undefined,
@@ -310,6 +312,8 @@ export class SafePassage {
310
312
  const result = {
311
313
  sessionId: event.data.sessionId,
312
314
  status: event.data.status,
315
+ timestamp: event.data.timestamp,
316
+ externalUserId: event.data.externalUserId,
313
317
  };
314
318
  // Log successful verification completion
315
319
  logSecurityEvent('VERIFICATION_COMPLETED', {
@@ -609,7 +613,7 @@ export class SafePassage {
609
613
  // Store the handoffToken if it exists for desktop QR flow
610
614
  if (sessionData.handoffToken) {
611
615
  // Store it temporarily so it can be included in the state
612
- this._temporaryHandoffToken = sessionData.handoffToken;
616
+ this.temporaryHandoffToken = sessionData.handoffToken;
613
617
  }
614
618
  // Log successful session creation
615
619
  logSecurityEvent('INTERNAL_SESSION_CREATED', {
package/dist/index.d.ts CHANGED
@@ -18,4 +18,4 @@
18
18
  */
19
19
  export { SafePassage, SafePassage as default } from './core/SafePassageSDK';
20
20
  export type { SafePassageConfig, VerificationOptions, VerificationResult, SessionValidationResponse, } from './types';
21
- export declare const VERSION = "3.4.2";
21
+ export declare const VERSION = "3.4.4";
package/dist/index.js CHANGED
@@ -1,40 +1,1086 @@
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.2: Fixed CDN references, aligned documentation
28
- export const VERSION = '3.4.2';
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
+ timestamp: event.data.timestamp,
725
+ externalUserId: event.data.externalUserId
726
+ };
727
+ logSecurityEvent("VERIFICATION_COMPLETED", {
728
+ status: result.status,
729
+ sessionId: sessionId.substring(0, 8) + "...",
730
+ origin: event.origin
731
+ });
732
+ this.cleanup();
733
+ this.unlockVerification();
734
+ if (this.popupMonitorInterval) {
735
+ clearInterval(this.popupMonitorInterval);
736
+ this.popupMonitorInterval = null;
737
+ }
738
+ if (result.status === "verified") {
739
+ (_d = (_c = this.config).onComplete) == null ? void 0 : _d.call(_c, result);
740
+ } else {
741
+ (_f = (_e = this.config).onError) == null ? void 0 : _f.call(
742
+ _e,
743
+ new Error(`Verification failed: ${result.status}`)
744
+ );
745
+ }
746
+ };
747
+ window.addEventListener("message", this.messageListener);
748
+ this.popupMonitorInterval = setInterval(() => {
749
+ var _a2, _b2;
750
+ if (this.popupWindow && this.popupWindow.closed) {
751
+ logSecurityEvent("POPUP_CLOSED_BY_USER", {
752
+ sessionId: sessionId.substring(0, 8) + "...",
753
+ environment: this.config.environment
754
+ });
755
+ this.cleanup();
756
+ this.unlockVerification();
757
+ (_b2 = (_a2 = this.config).onCancel) == null ? void 0 : _b2.call(_a2);
758
+ }
759
+ }, 500);
760
+ }
761
+ /**
762
+ * Set up automatic cleanup on page unload to prevent memory leaks
763
+ *
764
+ * Registers event listeners for page unload events to ensure proper
765
+ * cleanup of resources and verification state. Handles both traditional
766
+ * page navigation and single-page application route changes.
767
+ *
768
+ * @private
769
+ */
770
+ setupAutoCleanup() {
771
+ this.unloadListener = () => {
772
+ logSecurityEvent("SDK_AUTO_CLEANUP", {
773
+ environment: this.config.environment,
774
+ trigger: "page_unload"
775
+ });
776
+ this.cleanup();
777
+ this.unlockVerification();
778
+ };
779
+ window.addEventListener("beforeunload", this.unloadListener);
780
+ window.addEventListener("pagehide", this.unloadListener);
781
+ if (window.history && window.history.pushState) {
782
+ const originalPushState = window.history.pushState;
783
+ window.history.pushState = (...args) => {
784
+ this.cleanup();
785
+ this.unlockVerification();
786
+ return originalPushState.apply(window.history, args);
787
+ };
788
+ }
789
+ }
790
+ /**
791
+ * Auto-detect environment based on current URL
792
+ *
793
+ * Analyzes the current hostname to determine the appropriate environment
794
+ * configuration. Used when environment is not explicitly specified.
795
+ * Always defaults to production unless staging is detected.
796
+ *
797
+ * @returns {'production' | 'staging'} Detected environment
798
+ * @private
799
+ */
800
+ detectEnvironment() {
801
+ const hostname = window.location.hostname;
802
+ if (hostname.includes("staging") || hostname.includes("stage")) {
803
+ return "staging";
804
+ }
805
+ return "production";
806
+ }
807
+ /**
808
+ * Get the current environment
809
+ * @returns {string} The current environment (production or staging)
810
+ */
811
+ getEnvironment() {
812
+ return this.config.environment;
813
+ }
814
+ /**
815
+ * Unlock verification process to allow new verifications
816
+ *
817
+ * Resets the verification lock state to allow new verification attempts.
818
+ * Called after successful completion, errors, or cleanup.
819
+ *
820
+ * @private
821
+ */
822
+ unlockVerification() {
823
+ this.isVerificationInProgress = false;
824
+ this.currentSessionId = null;
825
+ logSecurityEvent("VERIFICATION_UNLOCKED", {
826
+ environment: this.config.environment,
827
+ origin: window.location.origin
828
+ });
829
+ }
830
+ /**
831
+ * Internal cleanup method to prevent memory leaks
832
+ *
833
+ * Cleans up popup windows, event listeners, and intervals.
834
+ * Does not unlock verification state - that's handled by specific callers.
835
+ *
836
+ * @private
837
+ */
838
+ cleanup() {
839
+ if (this.popupWindow && !this.popupWindow.closed) {
840
+ this.popupWindow.close();
841
+ }
842
+ this.popupWindow = null;
843
+ if (this.messageListener) {
844
+ window.removeEventListener("message", this.messageListener);
845
+ this.messageListener = null;
846
+ }
847
+ if (this.popupMonitorInterval) {
848
+ clearInterval(this.popupMonitorInterval);
849
+ this.popupMonitorInterval = null;
850
+ }
851
+ }
852
+ /**
853
+ * Remove auto-cleanup listeners
854
+ *
855
+ * Removes page unload event listeners that were set up for automatic cleanup.
856
+ *
857
+ * @private
858
+ */
859
+ removeAutoCleanupListeners() {
860
+ if (this.unloadListener) {
861
+ window.removeEventListener("beforeunload", this.unloadListener);
862
+ window.removeEventListener("pagehide", this.unloadListener);
863
+ this.unloadListener = null;
864
+ }
865
+ }
866
+ /**
867
+ * Public cleanup method for manual resource management
868
+ *
869
+ * Completely destroys the SDK instance, cleaning up all resources and
870
+ * removing all event listeners. Should be called when the SDK is no longer needed.
871
+ *
872
+ * @public
873
+ */
874
+ destroy() {
875
+ logSecurityEvent("SDK_DESTROYED", {
876
+ environment: this.config.environment,
877
+ origin: window.location.origin
878
+ });
879
+ this.cleanup();
880
+ this.unlockVerification();
881
+ this.removeAutoCleanupListeners();
882
+ }
883
+ /**
884
+ * Get Portal API URL based on environment
885
+ *
886
+ * Returns the appropriate portal-api URL for the current environment.
887
+ *
888
+ * @returns {string} Portal API URL
889
+ * @private
890
+ */
891
+ getPortalApiUrl() {
892
+ switch (this.config.environment) {
893
+ case "staging":
894
+ return "https://api.staging.safepassageapp.com";
895
+ case "production":
896
+ return "https://api.safepassageapp.com";
897
+ default:
898
+ return "https://api.safepassageapp.com";
899
+ }
900
+ }
901
+ /**
902
+ * Get Engine URL based on environment
903
+ *
904
+ * Returns the appropriate verify-engine URL for the current environment.
905
+ * In production, engine access is proxied through portal-api.
906
+ *
907
+ * @returns {string} Engine URL
908
+ * @private
909
+ */
910
+ getEngineUrl() {
911
+ switch (this.config.environment) {
912
+ case "staging":
913
+ return "https://engine.staging.safepassageapp.com";
914
+ case "production":
915
+ return "https://engine.safepassageapp.com";
916
+ default:
917
+ return "https://engine.safepassageapp.com";
918
+ }
919
+ }
920
+ /**
921
+ * Get WebSocket URL based on environment
922
+ *
923
+ * Returns the appropriate WebSocket URL for the current environment.
924
+ * In production, WebSocket connections are proxied through portal-api.
925
+ *
926
+ * @returns {string} WebSocket URL
927
+ * @private
928
+ */
929
+ getWebSocketUrl() {
930
+ switch (this.config.environment) {
931
+ case "staging":
932
+ return "wss://engine.staging.safepassageapp.com/api/websocket/stream";
933
+ case "production":
934
+ return "wss://engine.safepassageapp.com/api/websocket/stream";
935
+ default:
936
+ return "wss://engine.safepassageapp.com/api/websocket/stream";
937
+ }
938
+ }
939
+ /**
940
+ * Detect if this is a public key (pk_ prefix) vs private key (sk_ prefix)
941
+ *
942
+ * Determines API key type based on prefix to handle different authentication flows.
943
+ *
944
+ * @returns {boolean} True if public key, false if private key
945
+ * @private
946
+ */
947
+ isPublicKey() {
948
+ return this.config.apiKey.startsWith("pk_");
949
+ }
950
+ /**
951
+ * Create session internally for public keys
952
+ *
953
+ * Creates a verification session via the portal API for public key authentication.
954
+ * Generates a UUID session ID and submits session creation request with
955
+ * verification parameters.
956
+ *
957
+ * @param {VerificationOptions} options - Verification options
958
+ * @returns {Promise<string>} Created session ID
959
+ * @throws {Error} If session creation fails
960
+ * @private
961
+ */
962
+ async createInternalSession(options) {
963
+ var _a, _b;
964
+ try {
965
+ const portalApiUrl = this.getPortalApiUrl();
966
+ const response = await fetch(`${portalApiUrl}/api/v1/sessions/create`, {
967
+ method: "POST",
968
+ headers: {
969
+ "Content-Type": "application/json",
970
+ Authorization: `Bearer ${this.config.apiKey}`
971
+ },
972
+ body: JSON.stringify({
973
+ merchantId: this.config.apiKey,
974
+ // API expects merchantId even though it uses auth header
975
+ returnUrl: this.config.returnUrl,
976
+ cancelUrl: this.config.cancelUrl,
977
+ challengeAge: options.challengeAge,
978
+ verificationMode: options.verificationMode,
979
+ merchantName: document.title || window.location.hostname,
980
+ externalUserId: options.externalUserId
981
+ })
982
+ });
983
+ if (!response.ok) {
984
+ const errorData = await response.json().catch(() => ({}));
985
+ throw new Error(
986
+ `Failed to create session: ${response.status} ${response.statusText}. ${errorData.message || ""}`
987
+ );
988
+ }
989
+ const sessionData = await response.json();
990
+ const sessionId = sessionData.sessionId;
991
+ if (!sessionId) {
992
+ throw new Error("Server did not return a sessionId");
993
+ }
994
+ if (sessionData.verifyUrl) {
995
+ this.lastVerifyUrl = sessionData.verifyUrl;
996
+ }
997
+ if (sessionData.sessionToken) {
998
+ this.lastSessionToken = sessionData.sessionToken;
999
+ }
1000
+ if (sessionData.handoffToken) {
1001
+ this.temporaryHandoffToken = sessionData.handoffToken;
1002
+ }
1003
+ logSecurityEvent("INTERNAL_SESSION_CREATED", {
1004
+ sessionId: sessionId.substring(0, 8) + "...",
1005
+ environment: this.config.environment,
1006
+ apiKeyType: "public"
1007
+ });
1008
+ return sessionId;
1009
+ } catch (error) {
1010
+ const errorMessage = error instanceof Error ? error.message : String(error);
1011
+ logSecurityEvent("INTERNAL_SESSION_FAILED", {
1012
+ error: errorMessage,
1013
+ environment: this.config.environment,
1014
+ apiKeyType: "public"
1015
+ });
1016
+ (_b = (_a = this.config).onError) == null ? void 0 : _b.call(_a, error);
1017
+ throw new Error(`Failed to create verification session: ${errorMessage}`);
1018
+ }
1019
+ }
1020
+ };
1021
+ SafePassageSDK_default = SafePassage;
1022
+ }
1023
+ });
1024
+
1025
+ // src-redirect/utils/polyfills.ts
1026
+ function setupPolyfills() {
1027
+ if (!crypto.randomUUID) {
1028
+ crypto.randomUUID = function() {
1029
+ const array = new Uint8Array(16);
1030
+ crypto.getRandomValues(array);
1031
+ array[6] = array[6] & 15 | 64;
1032
+ array[8] = array[8] & 63 | 128;
1033
+ const hex = Array.from(array).map((b) => b.toString(16).padStart(2, "0")).join("");
1034
+ return [
1035
+ hex.slice(0, 8),
1036
+ hex.slice(8, 12),
1037
+ hex.slice(12, 16),
1038
+ hex.slice(16, 20),
1039
+ hex.slice(20, 32)
1040
+ ].join("-");
1041
+ };
1042
+ }
1043
+ }
1044
+ function checkBrowserCompatibility() {
1045
+ const warnings = [];
1046
+ if (!window.crypto || !window.crypto.getRandomValues) {
1047
+ throw new Error("SafePassage SDK requires Web Crypto API support");
1048
+ }
1049
+ if (!window.crypto.subtle) {
1050
+ throw new Error(
1051
+ "SafePassage SDK requires Web Crypto subtle API for HMAC operations"
1052
+ );
1053
+ }
1054
+ if (!crypto.randomUUID) {
1055
+ warnings.push("crypto.randomUUID not supported, using polyfill");
1056
+ }
1057
+ if (!window.URLSearchParams) {
1058
+ warnings.push(
1059
+ "URLSearchParams not supported, consider adding a polyfill for IE 11 support"
1060
+ );
1061
+ }
1062
+ if (warnings.length > 0) {
1063
+ console.warn("SafePassage SDK Browser Compatibility:", warnings.join("; "));
1064
+ }
1065
+ }
1066
+
1067
+ // src-redirect/index.ts
1068
+ init_SafePassageSDK();
1069
+ if (typeof window !== "undefined") {
1070
+ setupPolyfills();
1071
+ checkBrowserCompatibility();
1072
+ }
1073
+ var VERSION = "3.4.4";
1074
+ if (typeof window !== "undefined" && window) {
1075
+ const { SafePassage: SafePassage2 } = (init_SafePassageSDK(), __toCommonJS(SafePassageSDK_exports));
1076
+ const globalWindow = window;
1077
+ globalWindow.SafePassage = SafePassage2;
1078
+ if (globalWindow.SafePassage) {
1079
+ globalWindow.SafePassage.VERSION = VERSION;
1080
+ }
40
1081
  }
1082
+ export {
1083
+ SafePassage,
1084
+ VERSION,
1085
+ SafePassage as default
1086
+ };
@@ -1,3 +1,3 @@
1
- /* SafePassage SDK v3.4.2 */
2
- "use strict";var SafePassageSDK=(()=>{var v=Object.defineProperty,se=Object.defineProperties,oe=Object.getOwnPropertyDescriptor,ae=Object.getOwnPropertyDescriptors,ce=Object.getOwnPropertyNames,w=Object.getOwnPropertySymbols;var I=Object.prototype.hasOwnProperty,R=Object.prototype.propertyIsEnumerable;var L=(t,e,n)=>e in t?v(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,h=(t,e)=>{for(var n in e||(e={}))I.call(e,n)&&L(t,n,e[n]);if(w)for(var n of w(e))R.call(e,n)&&L(t,n,e[n]);return t},S=(t,e)=>se(t,ae(e));var C=(t,e)=>{var n={};for(var i in t)I.call(t,i)&&e.indexOf(i)<0&&(n[i]=t[i]);if(t!=null&&w)for(var i of w(t))e.indexOf(i)<0&&R.call(t,i)&&(n[i]=t[i]);return n};var m=(t,e)=>()=>(t&&(e=t(t=0)),e);var P=(t,e)=>{for(var n in e)v(t,n,{get:e[n],enumerable:!0})},le=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of ce(e))!I.call(t,r)&&r!==n&&v(t,r,{get:()=>e[r],enumerable:!(i=oe(e,r))||i.enumerable});return t};var D=t=>le(v({},"__esModule",{value:!0}),t);function pe(t,e){return N[e].includes(t)}function K(t,e,n=[]){var r;let{origin:i}=t;return pe(i,e)||n.length>0&&n.some(o=>{if(o.startsWith("*.")){let a=o.slice(2);return i.endsWith(`.${a}`)||i===`https://${a}`||i===`http://${a}`}return i===o})?!0:(console.warn(`SafePassage Security: Blocked PostMessage from untrusted origin: ${i}`,{environment:e,trustedOrigins:N[e],allowedCustomOrigins:n,eventType:(r=t.data)==null?void 0:r.type}),!1)}function W(t,e){let{data:n}=t;return!n||typeof n!="object"?{isValid:!1,error:"Invalid message format"}:n.type!=="safepassage:verification:complete"?{isValid:!1,error:"Invalid message type"}:!n.sessionId||n.sessionId!==e?{isValid:!1,error:"Session ID mismatch"}:!n.status||!["verified","failed"].includes(n.status)?{isValid:!1,error:"Invalid status value"}:{isValid:!0}}function H(t){t==="production"&&window.location.protocol!=="https:"&&console.warn("SafePassage Warning: HTTPS recommended for production environment",{current:window.location.href})}function A(t,e){try{let n=new URL(t);if(n.protocol!=="https:"&&!(n.hostname==="localhost"||n.hostname==="127.0.0.1"))return{isValid:!1,error:`HTTPS required for return URLs in ${e}`};let i=[/data:/i,/javascript:/i,/vbscript:/i,/file:/i,/ftp:/i];for(let r of i)if(r.test(t))return{isValid:!1,error:"Blocked suspicious URL scheme"};return{isValid:!0}}catch(n){return{isValid:!1,error:"Invalid URL format"}}}function l(t,e){console.warn(`SafePassage Security Event: ${t}`,h({timestamp:new Date().toISOString(),userAgent:navigator.userAgent,url:window.location.href},e))}var N,U,j,T=m(()=>{"use strict";N={production:["https://av.safepassageapp.com","https://portal.safepassageapp.com","https://api.safepassageapp.com"],staging:["https://av.staging.safepassageapp.com","https://portal.staging.safepassageapp.com","https://api.staging.safepassageapp.com"]};U=class{constructor(){this.attempts=new Map;this.maxAttempts=5;this.timeWindow=6e4}isAllowed(e){let n=Date.now(),r=(this.attempts.get(e)||[]).filter(s=>n-s<this.timeWindow);return r.length>=this.maxAttempts?(console.warn(`SafePassage Security: Rate limit exceeded for ${e}`),!1):(r.push(n),this.attempts.set(e,r),!0)}reset(e){this.attempts.delete(e)}},j=new U});var B={};P(B,{createSignedState:()=>ue,generateHMAC:()=>b,generateSecureToken:()=>F,getSigningSecret:()=>x,parseSignedState:()=>ge,verifyHMAC:()=>q});async function b(t,e){let n=new TextEncoder,i=n.encode(e),r=n.encode(t),s=await crypto.subtle.importKey("raw",i,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),o=await crypto.subtle.sign("HMAC",s,r);return Array.from(new Uint8Array(o)).map(a=>a.toString(16).padStart(2,"0")).join("")}async function q(t,e,n){try{let i=await b(t,n);return de(e,i)}catch(i){return!1}}function de(t,e){if(t.length!==e.length)return!1;let n=0;for(let i=0;i<t.length;i++)n|=t.charCodeAt(i)^e.charCodeAt(i);return n===0}function F(t=32){let e=new Uint8Array(t);return crypto.getRandomValues(e),Array.from(e,n=>n.toString(16).padStart(2,"0")).join("")}function x(t){return{production:"safepassage-prod-hmac-2025",staging:"safepassage-stage-hmac-2025"}[t]}async function ue(t,e){let n=S(h({},t),{timestamp:Date.now(),nonce:F(16)}),i=JSON.stringify(n),r=x(e),s=await b(i,r);return btoa(JSON.stringify({data:n,signature:s}))}async function ge(t,e,n=J){try{let r=atob(t),s=JSON.parse(r);if(!s.data||!s.signature)return console.warn("SafePassage: Invalid signed state format"),null;let{data:o,signature:a}=s,p=JSON.stringify(o),c=x(e);if(!await q(p,a,c))return console.warn("SafePassage: State signature verification failed"),null;if(o.timestamp){let _=Date.now()-o.timestamp;if(_>n)return console.warn("SafePassage: State parameter expired",{age:_,maxAge:n}),null}let i=o,{timestamp:g,nonce:f}=i;return C(i,["timestamp","nonce"])}catch(r){return console.warn("SafePassage: Failed to parse signed state",r),null}}var G=m(()=>{"use strict";V()});function Z(t){if(!t.apiKey)throw new Error("apiKey is required");if(t.apiKey.length>z)throw new Error(`apiKey exceeds maximum length of ${z} characters`);if(!fe.test(t.apiKey))throw new Error("Invalid apiKey format. Expected pk_xxx (public) or sk_xxx (private)");if(typeof window!="undefined"&&t.apiKey.startsWith("sk_"))throw new Error("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");if(!t.returnUrl)throw new Error("returnUrl is required");if(t.returnUrl.length>y)throw new Error(`returnUrl exceeds maximum length of ${y} characters`);let e=he(),n=A(t.returnUrl,e);if(!n.isValid)throw new Error(`returnUrl validation failed: ${n.error}`);if(t.cancelUrl){if(t.cancelUrl.length>y)throw new Error(`cancelUrl exceeds maximum length of ${y} characters`);let i=A(t.cancelUrl,e);if(!i.isValid)throw new Error(`cancelUrl validation failed: ${i.error}`)}if(t.defaultChallengeAge!==void 0){if(t.defaultChallengeAge<X)throw new Error(`defaultChallengeAge must be at least ${X}`);if(t.defaultChallengeAge>Y)throw new Error(`defaultChallengeAge cannot exceed ${Y}`)}if(t.defaultVerificationMode&&!["L1","L2"].includes(t.defaultVerificationMode))throw new Error("defaultVerificationMode must be L1 or L2");if(t.mode&&!["redirect","new-tab"].includes(t.mode))throw new Error("mode must be redirect or new-tab")}function he(){if(typeof window=="undefined")return"production";let t=window.location.hostname;return t.includes("staging")||t.includes("stage")?"staging":"production"}async function Q(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(G(),B));return n(t,e)}var X,Y,y,z,J,fe,V=m(()=>{"use strict";T();X=25,Y=150,y=2048,z=128,J=6e5,fe=/^(pk_|sk_)[a-zA-Z0-9_]+$/});function k(t){let e=ee[t]||ee.production;if(!e||!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function me(t){let e={production:"https://api.safepassageapp.com",staging:"https://api.staging.safepassageapp.com"},n=e[t]||e.production;if(!n||!n.startsWith("https://"))throw new Error(`HTTPS required for API URLs in ${t} environment`);return n}function te(t){let e=window.location.protocol==="https:";switch(t){case"production":e||console.warn("SafePassage Warning: HTTPS recommended for production environment");break;case"staging":e||console.warn("SafePassage Warning: HTTPS strongly recommended in staging environment");break}try{k(t),me(t)}catch(n){let i=n instanceof Error?n.message:String(n);throw new Error(`Environment configuration validation failed: ${i}`)}}var ee,ne=m(()=>{"use strict";ee={production:"https://av.safepassageapp.com",staging:"https://av.staging.safepassageapp.com"}});var ie={};P(ie,{SafePassage:()=>u,default:()=>we});var u,we,M=m(()=>{"use strict";V();ne();T();u=class{constructor(e){this.popupWindow=null;this.messageListener=null;this.popupMonitorInterval=null;this.unloadListener=null;this.isVerificationInProgress=!1;this.currentSessionId=null;this.lastVerifyUrl=null;this.lastSessionToken=null;Z(e);let n=e.environment||this.detectEnvironment();n!=="staging"&&n!=="production"&&(console.warn(`SafePassage SDK: Unknown environment '${n}', defaulting to 'production'`),n="production"),this.config=S(h({},e),{environment:n,mode:e.mode||"redirect"}),te(this.config.environment),H(this.config.environment),l("SDK_INITIALIZED",{environment:this.config.environment,mode:this.config.mode,origin:window.location.origin,protocol:window.location.protocol,hostname:window.location.hostname}),this.setupAutoCleanup()}async verify(e={}){var r,s,o,a,p,c;let n=this.isPublicKey(),i;if(n)i=await this.createInternalSession(e);else throw new Error("Private API keys (sk_) should use the direct API, not the SDK. The SDK is designed for browser-based public key usage only.");if(!i)throw new Error("Failed to obtain sessionId from server");if(this.isVerificationInProgress){let d=new Error(`Verification already in progress for session ${(r=this.currentSessionId)==null?void 0:r.substring(0,8)}...`);throw l("RACE_CONDITION_PREVENTED",{currentSession:((s=this.currentSessionId)==null?void 0:s.substring(0,8))+"...",attemptedSession:"new-session-attempt",origin:window.location.origin}),(a=(o=this.config).onError)==null||a.call(o,d),d}this.isVerificationInProgress=!0,this.currentSessionId=i;try{let d=`${this.config.apiKey}:${window.location.origin}`;if(!j.isAllowed(d)){let f=new Error("Too many verification attempts. Please wait before trying again.");throw l("RATE_LIMIT_EXCEEDED",{apiKey:this.config.apiKey.substring(0,8)+"...",origin:window.location.origin,sessionId:i?i.substring(0,8)+"...":"undefined"}),(c=(p=this.config).onError)==null||c.call(p,f),f}let g=await this.buildVerificationUrl(e,i);l("VERIFICATION_INITIATED",{environment:this.config.environment,mode:this.config.mode,sessionId:i?i.substring(0,8)+"...":"undefined",origin:window.location.origin}),this.config.mode==="new-tab"?this.openNewTab(g,i):(this.unlockVerification(),this.redirect(g))}catch(d){throw this.unlockVerification(),d}}async buildVerificationUrl(e,n){let i=k(this.config.environment),r=e.challengeAge!==void 0,s=e.verificationMode!==void 0,o=r||s,a=await Q({merchantId:this.config.apiKey,sessionId:n,returnUrl:this.config.returnUrl,cancelUrl:this.config.cancelUrl,challengeAge:e.challengeAge||this.config.defaultChallengeAge,verificationMode:e.verificationMode||this.config.defaultVerificationMode,hasOverrides:o,externalUserId:e.externalUserId,timestamp:Date.now(),apiUrl:this.getPortalApiUrl(),engineUrl:this.getEngineUrl(),wsUrl:this.getWebSocketUrl(),environment:this.config.environment,features:{testMode:!1,warmupPeriodMs:500,qualityThreshold:.6},handoffToken:this._temporaryHandoffToken,sessionToken:this.lastSessionToken||void 0,verifyUrl:this.lastVerifyUrl||void 0},this.config.environment);if(this.lastVerifyUrl)try{let c=new URL(this.lastVerifyUrl);return c.searchParams.set("state",a),c.searchParams.set("mode",this.config.mode),e.skipIntro&&c.searchParams.set("skip_intro","true"),e.autoReturn&&c.searchParams.set("auto_return","true"),c.toString()}catch(c){}let p=new URLSearchParams({state:a,sessionId:n,mode:this.config.mode});return e.skipIntro&&p.set("skip_intro","true"),e.autoReturn&&p.set("auto_return","true"),`${i}/?${p.toString()}`}redirect(e){window.location.href=e}openNewTab(e,n){var i,r;if(this.cleanup(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),this.popupWindow=window.open(e,"safepassage-verify","width=600,height=700"),!this.popupWindow){(r=(i=this.config).onError)==null||r.call(i,new Error("Failed to open verification window. Please check popup blocker settings."));return}this.messageListener=s=>{var p,c,d,g,f,E;if(!K(s,this.config.environment)){l("POSTMESSAGE_ORIGIN_BLOCKED",{origin:s.origin,environment:this.config.environment,expectedOrigins:`SafePassage trusted origins for ${this.config.environment}`,messageType:(p=s.data)==null?void 0:p.type});return}let o=W(s,n);if(!o.isValid){l("POSTMESSAGE_VALIDATION_FAILED",{error:o.error,origin:s.origin,sessionId:n.substring(0,8)+"...",messageType:(c=s.data)==null?void 0:c.type});return}let a={sessionId:s.data.sessionId,status:s.data.status};l("VERIFICATION_COMPLETED",{status:a.status,sessionId:n.substring(0,8)+"...",origin:s.origin}),this.cleanup(),this.unlockVerification(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),a.status==="verified"?(g=(d=this.config).onComplete)==null||g.call(d,a):(E=(f=this.config).onError)==null||E.call(f,new Error(`Verification failed: ${a.status}`))},window.addEventListener("message",this.messageListener),this.popupMonitorInterval=setInterval(()=>{var s,o;this.popupWindow&&this.popupWindow.closed&&(l("POPUP_CLOSED_BY_USER",{sessionId:n.substring(0,8)+"...",environment:this.config.environment}),this.cleanup(),this.unlockVerification(),(o=(s=this.config).onCancel)==null||o.call(s))},500)}setupAutoCleanup(){if(this.unloadListener=()=>{l("SDK_AUTO_CLEANUP",{environment:this.config.environment,trigger:"page_unload"}),this.cleanup(),this.unlockVerification()},window.addEventListener("beforeunload",this.unloadListener),window.addEventListener("pagehide",this.unloadListener),window.history&&window.history.pushState){let e=window.history.pushState;window.history.pushState=(...n)=>(this.cleanup(),this.unlockVerification(),e.apply(window.history,n))}}detectEnvironment(){let e=window.location.hostname;return e.includes("staging")||e.includes("stage")?"staging":"production"}getEnvironment(){return this.config.environment}unlockVerification(){this.isVerificationInProgress=!1,this.currentSessionId=null,l("VERIFICATION_UNLOCKED",{environment:this.config.environment,origin:window.location.origin})}cleanup(){this.popupWindow&&!this.popupWindow.closed&&this.popupWindow.close(),this.popupWindow=null,this.messageListener&&(window.removeEventListener("message",this.messageListener),this.messageListener=null),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null)}removeAutoCleanupListeners(){this.unloadListener&&(window.removeEventListener("beforeunload",this.unloadListener),window.removeEventListener("pagehide",this.unloadListener),this.unloadListener=null)}destroy(){l("SDK_DESTROYED",{environment:this.config.environment,origin:window.location.origin}),this.cleanup(),this.unlockVerification(),this.removeAutoCleanupListeners()}getPortalApiUrl(){switch(this.config.environment){case"staging":return"https://api.staging.safepassageapp.com";case"production":return"https://api.safepassageapp.com";default:return"https://api.safepassageapp.com"}}getEngineUrl(){switch(this.config.environment){case"staging":return"https://engine.staging.safepassageapp.com";case"production":return"https://engine.safepassageapp.com";default:return"https://engine.safepassageapp.com"}}getWebSocketUrl(){switch(this.config.environment){case"staging":return"wss://engine.staging.safepassageapp.com/api/websocket/stream";case"production":return"wss://engine.safepassageapp.com/api/websocket/stream";default:return"wss://engine.safepassageapp.com/api/websocket/stream"}}isPublicKey(){return this.config.apiKey.startsWith("pk_")}async createInternalSession(e){var n,i;try{let r=this.getPortalApiUrl(),s=await fetch(`${r}/api/v1/sessions/create`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({merchantId:this.config.apiKey,returnUrl:this.config.returnUrl,cancelUrl:this.config.cancelUrl,challengeAge:e.challengeAge,verificationMode:e.verificationMode,merchantName:document.title||window.location.hostname,externalUserId:e.externalUserId})});if(!s.ok){let p=await s.json().catch(()=>({}));throw new Error(`Failed to create session: ${s.status} ${s.statusText}. ${p.message||""}`)}let o=await s.json(),a=o.sessionId;if(!a)throw new Error("Server did not return a sessionId");return o.verifyUrl&&(this.lastVerifyUrl=o.verifyUrl),o.sessionToken&&(this.lastSessionToken=o.sessionToken),o.handoffToken&&(this._temporaryHandoffToken=o.handoffToken),l("INTERNAL_SESSION_CREATED",{sessionId:a.substring(0,8)+"...",environment:this.config.environment,apiKeyType:"public"}),a}catch(r){let s=r instanceof Error?r.message:String(r);throw l("INTERNAL_SESSION_FAILED",{error:s,environment:this.config.environment,apiKeyType:"public"}),(i=(n=this.config).onError)==null||i.call(n,r),new Error(`Failed to create verification session: ${s}`)}}},we=u});var ve={};P(ve,{SafePassage:()=>u,VERSION:()=>re,default:()=>u});function $(){crypto.randomUUID||(crypto.randomUUID=function(){let t=new Uint8Array(16);crypto.getRandomValues(t),t[6]=t[6]&15|64,t[8]=t[8]&63|128;let e=Array.from(t).map(n=>n.toString(16).padStart(2,"0")).join("");return[e.slice(0,8),e.slice(8,12),e.slice(12,16),e.slice(16,20),e.slice(20,32)].join("-")})}function O(){let t=[];if(!window.crypto||!window.crypto.getRandomValues)throw new Error("SafePassage SDK requires Web Crypto API support");if(!window.crypto.subtle)throw new Error("SafePassage SDK requires Web Crypto subtle API for HMAC operations");crypto.randomUUID||t.push("crypto.randomUUID not supported, using polyfill"),window.URLSearchParams||t.push("URLSearchParams not supported, consider adding a polyfill for IE 11 support"),t.length>0&&console.warn("SafePassage SDK Browser Compatibility:",t.join("; "))}M();typeof window!="undefined"&&($(),O());var re="3.4.2";if(typeof window!="undefined"&&window){let{SafePassage:t}=(M(),D(ie)),e=window;e.SafePassage=t,e.SafePassage&&(e.SafePassage.VERSION=re)}return D(ve);})();
1
+ /* SafePassage SDK v3.4.4 */
2
+ "use strict";var SafePassageSDK=(()=>{var v=Object.defineProperty,se=Object.defineProperties,oe=Object.getOwnPropertyDescriptor,ae=Object.getOwnPropertyDescriptors,ce=Object.getOwnPropertyNames,w=Object.getOwnPropertySymbols;var I=Object.prototype.hasOwnProperty,R=Object.prototype.propertyIsEnumerable;var _=(t,e,n)=>e in t?v(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,h=(t,e)=>{for(var n in e||(e={}))I.call(e,n)&&_(t,n,e[n]);if(w)for(var n of w(e))R.call(e,n)&&_(t,n,e[n]);return t},S=(t,e)=>se(t,ae(e));var C=(t,e)=>{var n={};for(var i in t)I.call(t,i)&&e.indexOf(i)<0&&(n[i]=t[i]);if(t!=null&&w)for(var i of w(t))e.indexOf(i)<0&&R.call(t,i)&&(n[i]=t[i]);return n};var m=(t,e)=>()=>(t&&(e=t(t=0)),e);var P=(t,e)=>{for(var n in e)v(t,n,{get:e[n],enumerable:!0})},le=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of ce(e))!I.call(t,s)&&s!==n&&v(t,s,{get:()=>e[s],enumerable:!(i=oe(e,s))||i.enumerable});return t};var D=t=>le(v({},"__esModule",{value:!0}),t);function pe(t,e){return N[e].includes(t)}function K(t,e,n=[]){var s;let{origin:i}=t;return pe(i,e)||n.length>0&&n.some(o=>{if(o.startsWith("*.")){let a=o.slice(2);return i.endsWith(`.${a}`)||i===`https://${a}`||i===`http://${a}`}return i===o})?!0:(console.warn(`SafePassage Security: Blocked PostMessage from untrusted origin: ${i}`,{environment:e,trustedOrigins:N[e],allowedCustomOrigins:n,eventType:(s=t.data)==null?void 0:s.type}),!1)}function W(t,e){let{data:n}=t;return!n||typeof n!="object"?{isValid:!1,error:"Invalid message format"}:n.type!=="safepassage:verification:complete"?{isValid:!1,error:"Invalid message type"}:!n.sessionId||n.sessionId!==e?{isValid:!1,error:"Session ID mismatch"}:!n.status||!["verified","failed"].includes(n.status)?{isValid:!1,error:"Invalid status value"}:{isValid:!0}}function H(t){t==="production"&&window.location.protocol!=="https:"&&console.warn("SafePassage Warning: HTTPS recommended for production environment",{current:window.location.href})}function A(t,e){try{let n=new URL(t);if(n.protocol!=="https:"&&!(n.hostname==="localhost"||n.hostname==="127.0.0.1"))return{isValid:!1,error:`HTTPS required for return URLs in ${e}`};let i=[/data:/i,/javascript:/i,/vbscript:/i,/file:/i,/ftp:/i];for(let s of i)if(s.test(t))return{isValid:!1,error:"Blocked suspicious URL scheme"};return{isValid:!0}}catch(n){return{isValid:!1,error:"Invalid URL format"}}}function l(t,e){console.warn(`SafePassage Security Event: ${t}`,h({timestamp:new Date().toISOString(),userAgent:navigator.userAgent,url:window.location.href},e))}var N,U,j,T=m(()=>{"use strict";N={production:["https://av.safepassageapp.com","https://portal.safepassageapp.com","https://api.safepassageapp.com"],staging:["https://av.staging.safepassageapp.com","https://portal.staging.safepassageapp.com","https://api.staging.safepassageapp.com"]};U=class{constructor(){this.attempts=new Map;this.maxAttempts=5;this.timeWindow=6e4}isAllowed(e){let n=Date.now(),s=(this.attempts.get(e)||[]).filter(r=>n-r<this.timeWindow);return s.length>=this.maxAttempts?(console.warn(`SafePassage Security: Rate limit exceeded for ${e}`),!1):(s.push(n),this.attempts.set(e,s),!0)}reset(e){this.attempts.delete(e)}},j=new U});var B={};P(B,{createSignedState:()=>ue,generateHMAC:()=>b,generateSecureToken:()=>F,getSigningSecret:()=>x,parseSignedState:()=>ge,verifyHMAC:()=>q});async function b(t,e){let n=new TextEncoder,i=n.encode(e),s=n.encode(t),r=await crypto.subtle.importKey("raw",i,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),o=await crypto.subtle.sign("HMAC",r,s);return Array.from(new Uint8Array(o)).map(a=>a.toString(16).padStart(2,"0")).join("")}async function q(t,e,n){try{let i=await b(t,n);return de(e,i)}catch(i){return!1}}function de(t,e){if(t.length!==e.length)return!1;let n=0;for(let i=0;i<t.length;i++)n|=t.charCodeAt(i)^e.charCodeAt(i);return n===0}function F(t=32){let e=new Uint8Array(t);return crypto.getRandomValues(e),Array.from(e,n=>n.toString(16).padStart(2,"0")).join("")}function x(t){return{production:"safepassage-prod-hmac-2025",staging:"safepassage-stage-hmac-2025"}[t]}async function ue(t,e){let n=S(h({},t),{timestamp:Date.now(),nonce:F(16)}),i=JSON.stringify(n),s=x(e),r=await b(i,s);return btoa(JSON.stringify({data:n,signature:r}))}async function ge(t,e,n=J){try{let s=atob(t),r=JSON.parse(s);if(!r.data||!r.signature)return console.warn("SafePassage: Invalid signed state format"),null;let{data:o,signature:a}=r,p=JSON.stringify(o),c=x(e);if(!await q(p,a,c))return console.warn("SafePassage: State signature verification failed"),null;if(o.timestamp){let L=Date.now()-o.timestamp;if(L>n)return console.warn("SafePassage: State parameter expired",{age:L,maxAge:n}),null}let i=o,{timestamp:g,nonce:f}=i;return C(i,["timestamp","nonce"])}catch(s){return console.warn("SafePassage: Failed to parse signed state",s),null}}var G=m(()=>{"use strict";V()});function Z(t){if(!t.apiKey)throw new Error("apiKey is required");if(t.apiKey.length>z)throw new Error(`apiKey exceeds maximum length of ${z} characters`);if(!fe.test(t.apiKey))throw new Error("Invalid apiKey format. Expected pk_xxx (public) or sk_xxx (private)");if(typeof window!="undefined"&&t.apiKey.startsWith("sk_"))throw new Error("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");if(!t.returnUrl)throw new Error("returnUrl is required");if(t.returnUrl.length>y)throw new Error(`returnUrl exceeds maximum length of ${y} characters`);let e=he(),n=A(t.returnUrl,e);if(!n.isValid)throw new Error(`returnUrl validation failed: ${n.error}`);if(t.cancelUrl){if(t.cancelUrl.length>y)throw new Error(`cancelUrl exceeds maximum length of ${y} characters`);let i=A(t.cancelUrl,e);if(!i.isValid)throw new Error(`cancelUrl validation failed: ${i.error}`)}if(t.defaultChallengeAge!==void 0){if(t.defaultChallengeAge<X)throw new Error(`defaultChallengeAge must be at least ${X}`);if(t.defaultChallengeAge>Y)throw new Error(`defaultChallengeAge cannot exceed ${Y}`)}if(t.defaultVerificationMode&&!["L1","L2"].includes(t.defaultVerificationMode))throw new Error("defaultVerificationMode must be L1 or L2");if(t.mode&&!["redirect","new-tab"].includes(t.mode))throw new Error("mode must be redirect or new-tab")}function he(){if(typeof window=="undefined")return"production";let t=window.location.hostname;return t.includes("staging")||t.includes("stage")?"staging":"production"}async function Q(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(G(),B));return n(t,e)}var X,Y,y,z,J,fe,V=m(()=>{"use strict";T();X=25,Y=150,y=2048,z=128,J=6e5,fe=/^(pk_|sk_)[a-zA-Z0-9_]+$/});function k(t){let e=ee[t]||ee.production;if(!e||!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function me(t){let e={production:"https://api.safepassageapp.com",staging:"https://api.staging.safepassageapp.com"},n=e[t]||e.production;if(!n||!n.startsWith("https://"))throw new Error(`HTTPS required for API URLs in ${t} environment`);return n}function te(t){let e=window.location.protocol==="https:";switch(t){case"production":e||console.warn("SafePassage Warning: HTTPS recommended for production environment");break;case"staging":e||console.warn("SafePassage Warning: HTTPS strongly recommended in staging environment");break}try{k(t),me(t)}catch(n){let i=n instanceof Error?n.message:String(n);throw new Error(`Environment configuration validation failed: ${i}`)}}var ee,ne=m(()=>{"use strict";ee={production:"https://av.safepassageapp.com",staging:"https://av.staging.safepassageapp.com"}});var ie={};P(ie,{SafePassage:()=>u,default:()=>we});var u,we,M=m(()=>{"use strict";V();ne();T();u=class{constructor(e){this.popupWindow=null;this.messageListener=null;this.popupMonitorInterval=null;this.unloadListener=null;this.isVerificationInProgress=!1;this.currentSessionId=null;this.lastVerifyUrl=null;this.lastSessionToken=null;this.temporaryHandoffToken=null;Z(e);let n=e.environment||this.detectEnvironment();n!=="staging"&&n!=="production"&&(console.warn(`SafePassage SDK: Unknown environment '${n}', defaulting to 'production'`),n="production"),this.config=S(h({},e),{environment:n,mode:e.mode||"redirect"}),te(this.config.environment),H(this.config.environment),l("SDK_INITIALIZED",{environment:this.config.environment,mode:this.config.mode,origin:window.location.origin,protocol:window.location.protocol,hostname:window.location.hostname}),this.setupAutoCleanup()}async verify(e={}){var s,r,o,a,p,c;let n=this.isPublicKey(),i;if(n)i=await this.createInternalSession(e);else throw new Error("Private API keys (sk_) should use the direct API, not the SDK. The SDK is designed for browser-based public key usage only.");if(!i)throw new Error("Failed to obtain sessionId from server");if(this.isVerificationInProgress){let d=new Error(`Verification already in progress for session ${(s=this.currentSessionId)==null?void 0:s.substring(0,8)}...`);throw l("RACE_CONDITION_PREVENTED",{currentSession:((r=this.currentSessionId)==null?void 0:r.substring(0,8))+"...",attemptedSession:"new-session-attempt",origin:window.location.origin}),(a=(o=this.config).onError)==null||a.call(o,d),d}this.isVerificationInProgress=!0,this.currentSessionId=i;try{let d=`${this.config.apiKey}:${window.location.origin}`;if(!j.isAllowed(d)){let f=new Error("Too many verification attempts. Please wait before trying again.");throw l("RATE_LIMIT_EXCEEDED",{apiKey:this.config.apiKey.substring(0,8)+"...",origin:window.location.origin,sessionId:i?i.substring(0,8)+"...":"undefined"}),(c=(p=this.config).onError)==null||c.call(p,f),f}let g=await this.buildVerificationUrl(e,i);l("VERIFICATION_INITIATED",{environment:this.config.environment,mode:this.config.mode,sessionId:i?i.substring(0,8)+"...":"undefined",origin:window.location.origin}),this.config.mode==="new-tab"?this.openNewTab(g,i):(this.unlockVerification(),this.redirect(g))}catch(d){throw this.unlockVerification(),d}}async buildVerificationUrl(e,n){let i=k(this.config.environment),s=e.challengeAge!==void 0,r=e.verificationMode!==void 0,o=s||r,a=await Q({merchantId:this.config.apiKey,sessionId:n,returnUrl:this.config.returnUrl,cancelUrl:this.config.cancelUrl,challengeAge:e.challengeAge||this.config.defaultChallengeAge,verificationMode:e.verificationMode||this.config.defaultVerificationMode,hasOverrides:o,externalUserId:e.externalUserId,timestamp:Date.now(),apiUrl:this.getPortalApiUrl(),engineUrl:this.getEngineUrl(),wsUrl:this.getWebSocketUrl(),environment:this.config.environment,features:{testMode:!1,warmupPeriodMs:500,qualityThreshold:.6},handoffToken:this.temporaryHandoffToken||void 0,sessionToken:this.lastSessionToken||void 0,verifyUrl:this.lastVerifyUrl||void 0},this.config.environment);if(this.lastVerifyUrl)try{let c=new URL(this.lastVerifyUrl);return c.searchParams.set("state",a),c.searchParams.set("mode",this.config.mode),e.skipIntro&&c.searchParams.set("skip_intro","true"),e.autoReturn&&c.searchParams.set("auto_return","true"),c.toString()}catch(c){}let p=new URLSearchParams({state:a,sessionId:n,mode:this.config.mode});return e.skipIntro&&p.set("skip_intro","true"),e.autoReturn&&p.set("auto_return","true"),`${i}/?${p.toString()}`}redirect(e){window.location.href=e}openNewTab(e,n){var i,s;if(this.cleanup(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),this.popupWindow=window.open(e,"safepassage-verify","width=600,height=700"),!this.popupWindow){(s=(i=this.config).onError)==null||s.call(i,new Error("Failed to open verification window. Please check popup blocker settings."));return}this.messageListener=r=>{var p,c,d,g,f,E;if(!K(r,this.config.environment)){l("POSTMESSAGE_ORIGIN_BLOCKED",{origin:r.origin,environment:this.config.environment,expectedOrigins:`SafePassage trusted origins for ${this.config.environment}`,messageType:(p=r.data)==null?void 0:p.type});return}let o=W(r,n);if(!o.isValid){l("POSTMESSAGE_VALIDATION_FAILED",{error:o.error,origin:r.origin,sessionId:n.substring(0,8)+"...",messageType:(c=r.data)==null?void 0:c.type});return}let a={sessionId:r.data.sessionId,status:r.data.status,timestamp:r.data.timestamp,externalUserId:r.data.externalUserId};l("VERIFICATION_COMPLETED",{status:a.status,sessionId:n.substring(0,8)+"...",origin:r.origin}),this.cleanup(),this.unlockVerification(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),a.status==="verified"?(g=(d=this.config).onComplete)==null||g.call(d,a):(E=(f=this.config).onError)==null||E.call(f,new Error(`Verification failed: ${a.status}`))},window.addEventListener("message",this.messageListener),this.popupMonitorInterval=setInterval(()=>{var r,o;this.popupWindow&&this.popupWindow.closed&&(l("POPUP_CLOSED_BY_USER",{sessionId:n.substring(0,8)+"...",environment:this.config.environment}),this.cleanup(),this.unlockVerification(),(o=(r=this.config).onCancel)==null||o.call(r))},500)}setupAutoCleanup(){if(this.unloadListener=()=>{l("SDK_AUTO_CLEANUP",{environment:this.config.environment,trigger:"page_unload"}),this.cleanup(),this.unlockVerification()},window.addEventListener("beforeunload",this.unloadListener),window.addEventListener("pagehide",this.unloadListener),window.history&&window.history.pushState){let e=window.history.pushState;window.history.pushState=(...n)=>(this.cleanup(),this.unlockVerification(),e.apply(window.history,n))}}detectEnvironment(){let e=window.location.hostname;return e.includes("staging")||e.includes("stage")?"staging":"production"}getEnvironment(){return this.config.environment}unlockVerification(){this.isVerificationInProgress=!1,this.currentSessionId=null,l("VERIFICATION_UNLOCKED",{environment:this.config.environment,origin:window.location.origin})}cleanup(){this.popupWindow&&!this.popupWindow.closed&&this.popupWindow.close(),this.popupWindow=null,this.messageListener&&(window.removeEventListener("message",this.messageListener),this.messageListener=null),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null)}removeAutoCleanupListeners(){this.unloadListener&&(window.removeEventListener("beforeunload",this.unloadListener),window.removeEventListener("pagehide",this.unloadListener),this.unloadListener=null)}destroy(){l("SDK_DESTROYED",{environment:this.config.environment,origin:window.location.origin}),this.cleanup(),this.unlockVerification(),this.removeAutoCleanupListeners()}getPortalApiUrl(){switch(this.config.environment){case"staging":return"https://api.staging.safepassageapp.com";case"production":return"https://api.safepassageapp.com";default:return"https://api.safepassageapp.com"}}getEngineUrl(){switch(this.config.environment){case"staging":return"https://engine.staging.safepassageapp.com";case"production":return"https://engine.safepassageapp.com";default:return"https://engine.safepassageapp.com"}}getWebSocketUrl(){switch(this.config.environment){case"staging":return"wss://engine.staging.safepassageapp.com/api/websocket/stream";case"production":return"wss://engine.safepassageapp.com/api/websocket/stream";default:return"wss://engine.safepassageapp.com/api/websocket/stream"}}isPublicKey(){return this.config.apiKey.startsWith("pk_")}async createInternalSession(e){var n,i;try{let s=this.getPortalApiUrl(),r=await fetch(`${s}/api/v1/sessions/create`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({merchantId:this.config.apiKey,returnUrl:this.config.returnUrl,cancelUrl:this.config.cancelUrl,challengeAge:e.challengeAge,verificationMode:e.verificationMode,merchantName:document.title||window.location.hostname,externalUserId:e.externalUserId})});if(!r.ok){let p=await r.json().catch(()=>({}));throw new Error(`Failed to create session: ${r.status} ${r.statusText}. ${p.message||""}`)}let o=await r.json(),a=o.sessionId;if(!a)throw new Error("Server did not return a sessionId");return o.verifyUrl&&(this.lastVerifyUrl=o.verifyUrl),o.sessionToken&&(this.lastSessionToken=o.sessionToken),o.handoffToken&&(this.temporaryHandoffToken=o.handoffToken),l("INTERNAL_SESSION_CREATED",{sessionId:a.substring(0,8)+"...",environment:this.config.environment,apiKeyType:"public"}),a}catch(s){let r=s instanceof Error?s.message:String(s);throw l("INTERNAL_SESSION_FAILED",{error:r,environment:this.config.environment,apiKeyType:"public"}),(i=(n=this.config).onError)==null||i.call(n,s),new Error(`Failed to create verification session: ${r}`)}}},we=u});var ve={};P(ve,{SafePassage:()=>u,VERSION:()=>re,default:()=>u});function $(){crypto.randomUUID||(crypto.randomUUID=function(){let t=new Uint8Array(16);crypto.getRandomValues(t),t[6]=t[6]&15|64,t[8]=t[8]&63|128;let e=Array.from(t).map(n=>n.toString(16).padStart(2,"0")).join("");return[e.slice(0,8),e.slice(8,12),e.slice(12,16),e.slice(16,20),e.slice(20,32)].join("-")})}function O(){let t=[];if(!window.crypto||!window.crypto.getRandomValues)throw new Error("SafePassage SDK requires Web Crypto API support");if(!window.crypto.subtle)throw new Error("SafePassage SDK requires Web Crypto subtle API for HMAC operations");crypto.randomUUID||t.push("crypto.randomUUID not supported, using polyfill"),window.URLSearchParams||t.push("URLSearchParams not supported, consider adding a polyfill for IE 11 support"),t.length>0&&console.warn("SafePassage SDK Browser Compatibility:",t.join("; "))}M();typeof window!="undefined"&&($(),O());var re="3.4.4";if(typeof window!="undefined"&&window){let{SafePassage:t}=(M(),D(ie)),e=window;e.SafePassage=t,e.SafePassage&&(e.SafePassage.VERSION=re)}return D(ve);})();
3
3
  if(typeof SafePassageSDK !== "undefined" && SafePassageSDK.SafePassage) { window.SafePassage = SafePassageSDK.SafePassage; window.SafePassage.VERSION = SafePassageSDK.VERSION; }
@@ -92,6 +92,10 @@ export interface VerificationResult {
92
92
  * Full details available via server-side API
93
93
  */
94
94
  status: 'verified' | 'failed';
95
+ /**
96
+ * Timestamp when verification completed (milliseconds since epoch)
97
+ */
98
+ timestamp?: number;
95
99
  /**
96
100
  * External user identifier if provided during verification
97
101
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safepassage/sdk",
3
- "version": "3.4.2",
3
+ "version": "3.4.4",
4
4
  "description": "SafePassage SDK - Lightweight redirect-based age verification",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,7 +10,8 @@
10
10
  "LICENSE"
11
11
  ],
12
12
  "scripts": {
13
- "build": "tsc && node build-sdk.js",
13
+ "build": "npm run clean && tsc && node build-sdk.js",
14
+ "clean": "rm -rf dist",
14
15
  "dev": "tsc --watch",
15
16
  "demo": "python3 -m http.server 8080",
16
17
  "serve-demo": "npx serve . -p 8080",