@safepassage/sdk 3.2.4 → 3.4.0

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
@@ -31,8 +31,7 @@ npm install @safepassage/sdk
31
31
  // Initialize SDK
32
32
  const safePassage = new SafePassage({
33
33
  apiKey: 'sk_xxxxx', // Your API key
34
- returnUrl: 'https://yoursite.com/verified',
35
- cancelUrl: 'https://yoursite.com/cancelled'
34
+ returnUrl: 'https://yoursite.com/verified'
36
35
  });
37
36
 
38
37
  // Generate a session ID (must be UUID v4)
@@ -52,11 +51,9 @@ safePassage.verify({
52
51
  |--------|------|----------|-------------|
53
52
  | apiKey | string | Yes | Your API key (pk_xxx for client-side, sk_xxx for server-side) |
54
53
  | returnUrl | string | Yes | URL to redirect after successful verification |
55
- | cancelUrl | string | Yes | URL to redirect if user cancels |
56
54
  | environment | string | No | 'production', 'staging', or 'development' (auto-detected) |
57
55
  | mode | string | No | 'redirect' (default) or 'new-tab' |
58
56
  | onComplete | function | No | Callback for new-tab mode completion |
59
- | onCancel | function | No | Callback for new-tab mode cancellation |
60
57
  | onError | function | No | Error handler |
61
58
 
62
59
  ### API Key Types
@@ -127,8 +124,7 @@ User is redirected to SafePassage, then back to your site:
127
124
  ```javascript
128
125
  const safePassage = new SafePassage({
129
126
  apiKey: 'sk_xxxxx',
130
- returnUrl: '/age-verified',
131
- cancelUrl: '/age-gate'
127
+ returnUrl: '/age-verified'
132
128
  });
133
129
 
134
130
  safePassage.verify({ sessionId: crypto.randomUUID() });
@@ -141,20 +137,54 @@ Verification opens in a popup window:
141
137
  const safePassage = new SafePassage({
142
138
  apiKey: 'sk_xxxxx',
143
139
  returnUrl: '/age-verified',
144
- cancelUrl: '/age-gate',
145
140
  mode: 'new-tab',
146
141
  onComplete: (result) => {
147
142
  console.log('Verified:', result.sessionId);
148
143
  // Validate on your server!
149
- },
150
- onCancel: () => {
151
- console.log('User cancelled');
152
144
  }
153
145
  });
154
146
 
155
147
  safePassage.verify({ sessionId: crypto.randomUUID() });
156
148
  ```
157
149
 
150
+ ## URL Query Parameters
151
+
152
+ SafePassage supports optional query parameters for streamlined verification flows:
153
+
154
+ ### skip_intro
155
+ Skip the introductory screen and navigate directly to camera access:
156
+
157
+ ```javascript
158
+ // Via SDK (automatic)
159
+ safePassage.verify({ sessionId: crypto.randomUUID() });
160
+
161
+ // Via server-side session creation
162
+ const url = new URL(verifyUrl);
163
+ url.searchParams.set('skip_intro', 'true');
164
+ window.location.href = url.toString();
165
+ ```
166
+
167
+ ### auto_return
168
+ Automatically redirect to `returnUrl` after successful verification (redirect mode only):
169
+
170
+ ```javascript
171
+ // Via server-side session creation
172
+ const url = new URL(verifyUrl);
173
+ url.searchParams.set('auto_return', 'true');
174
+ window.location.href = url.toString();
175
+ ```
176
+
177
+ ### Combined Example
178
+ ```javascript
179
+ const url = new URL(verifyUrl);
180
+ url.searchParams.set('skip_intro', 'true');
181
+ url.searchParams.set('auto_return', 'true');
182
+ // Result: Streamlined flow with minimal user interaction
183
+ window.location.href = url.toString();
184
+ ```
185
+
186
+ **For comprehensive parameter documentation, usage examples, and integration patterns, see [URL Parameters Reference](../../../docs/technical/api/VERIFY_UI_PARAMETERS.md).**
187
+
158
188
  ## Server-Side Validation (Required!)
159
189
 
160
190
  Always validate the session on your server:
@@ -209,8 +239,7 @@ function generateUUID() {
209
239
  <script>
210
240
  const safePassage = new SafePassage({
211
241
  apiKey: 'sk_xxxxx',
212
- returnUrl: window.location.href + '?verified=true',
213
- cancelUrl: window.location.href
242
+ returnUrl: window.location.href + '?verified=true'
214
243
  });
215
244
 
216
245
  function verifyAge() {
@@ -248,8 +277,7 @@ import { SafePassage, SafePassageConfig } from '@safepassage/sdk';
248
277
 
249
278
  const config: SafePassageConfig = {
250
279
  apiKey: process.env.SAFEPASSAGE_PUBLIC_KEY!,
251
- returnUrl: '/verified',
252
- cancelUrl: '/cancelled'
280
+ returnUrl: '/verified'
253
281
  };
254
282
 
255
283
  const safePassage = new SafePassage(config);
@@ -261,8 +289,7 @@ New streamlined approach (10 lines):
261
289
  ```javascript
262
290
  const safePassage = new SafePassage({
263
291
  apiKey: 'sk_xxxxx',
264
- returnUrl: '/verified',
265
- cancelUrl: '/cancelled'
292
+ returnUrl: '/verified'
266
293
  });
267
294
  safePassage.verify({ sessionId: crypto.randomUUID() });
268
295
  ```
@@ -280,4 +307,4 @@ safePassage.verify({ sessionId: crypto.randomUUID() });
280
307
  1. Always generate session IDs on the merchant side
281
308
  2. Validate sessions server-side before granting access
282
309
  3. Pre-register callback URLs in your dashboard
283
- 4. Never expose your secret key (sk_xxx)
310
+ 4. Never expose your secret key (sk_xxx)
@@ -70,11 +70,7 @@ export class SafePassage {
70
70
  console.warn(`SafePassage SDK: Unknown environment '${normalizedEnvironment}', defaulting to 'production'`);
71
71
  normalizedEnvironment = 'production';
72
72
  }
73
- this.config = {
74
- ...config,
75
- environment: normalizedEnvironment,
76
- mode: config.mode || 'redirect',
77
- };
73
+ this.config = Object.assign(Object.assign({}, config), { environment: normalizedEnvironment, mode: config.mode || 'redirect' });
78
74
  // Comprehensive environment security validation
79
75
  validateEnvironmentSecurity(this.config.environment);
80
76
  // Enforce HTTPS in production (additional layer)
@@ -109,6 +105,7 @@ export class SafePassage {
109
105
  * @throws {Error} If verification cannot be started or is already in progress
110
106
  */
111
107
  async verify(options = {}) {
108
+ var _a, _b, _c, _d, _e, _f;
112
109
  const isPublicKey = this.isPublicKey();
113
110
  // For public keys, create session via API (SafePassage generates the sessionId)
114
111
  let sessionId;
@@ -125,13 +122,13 @@ export class SafePassage {
125
122
  }
126
123
  // Race condition check - prevent multiple simultaneous verifications
127
124
  if (this.isVerificationInProgress) {
128
- const error = new Error(`Verification already in progress for session ${this.currentSessionId?.substring(0, 8)}...`);
125
+ const error = new Error(`Verification already in progress for session ${(_a = this.currentSessionId) === null || _a === void 0 ? void 0 : _a.substring(0, 8)}...`);
129
126
  logSecurityEvent('RACE_CONDITION_PREVENTED', {
130
- currentSession: this.currentSessionId?.substring(0, 8) + '...',
127
+ currentSession: ((_b = this.currentSessionId) === null || _b === void 0 ? void 0 : _b.substring(0, 8)) + '...',
131
128
  attemptedSession: 'new-session-attempt',
132
129
  origin: window.location.origin,
133
130
  });
134
- this.config.onError?.(error);
131
+ (_d = (_c = this.config).onError) === null || _d === void 0 ? void 0 : _d.call(_c, error);
135
132
  throw error;
136
133
  }
137
134
  // Lock verification process
@@ -149,7 +146,7 @@ export class SafePassage {
149
146
  ? sessionId.substring(0, 8) + '...'
150
147
  : 'undefined',
151
148
  });
152
- this.config.onError?.(error);
149
+ (_f = (_e = this.config).onError) === null || _f === void 0 ? void 0 : _f.call(_e, error);
153
150
  throw error;
154
151
  }
155
152
  const verificationUrl = await this.buildVerificationUrl(options, sessionId);
@@ -224,13 +221,28 @@ export class SafePassage {
224
221
  const url = new URL(this.lastVerifyUrl);
225
222
  url.searchParams.set('state', state);
226
223
  url.searchParams.set('mode', this.config.mode);
224
+ // Append skip parameters if provided
225
+ if (options.skipIntro) {
226
+ url.searchParams.set('skip_intro', 'true');
227
+ }
228
+ if (options.autoReturn) {
229
+ url.searchParams.set('auto_return', 'true');
230
+ }
227
231
  return url.toString();
228
232
  }
229
- catch {
233
+ catch (_a) {
230
234
  // Fall back to client-constructed URL if parsing fails
231
235
  }
232
236
  }
237
+ // Client-constructed URL fallback (for backwards compatibility)
233
238
  const params = new URLSearchParams({ state, sessionId, mode: this.config.mode });
239
+ // Append skip parameters if provided
240
+ if (options.skipIntro) {
241
+ params.set('skip_intro', 'true');
242
+ }
243
+ if (options.autoReturn) {
244
+ params.set('auto_return', 'true');
245
+ }
234
246
  return `${baseUrl}/?${params.toString()}`;
235
247
  }
236
248
  /**
@@ -257,6 +269,7 @@ export class SafePassage {
257
269
  * @private
258
270
  */
259
271
  openNewTab(url, sessionId) {
272
+ var _a, _b;
260
273
  // Clean up any existing resources
261
274
  this.cleanup();
262
275
  // Clear any existing popup monitor interval
@@ -267,18 +280,19 @@ export class SafePassage {
267
280
  // Open new tab
268
281
  this.popupWindow = window.open(url, 'safepassage-verify', 'width=600,height=700');
269
282
  if (!this.popupWindow) {
270
- this.config.onError?.(new Error('Failed to open verification window. Please check popup blocker settings.'));
283
+ (_b = (_a = this.config).onError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error('Failed to open verification window. Please check popup blocker settings.'));
271
284
  return;
272
285
  }
273
286
  // Set up PostMessage listener with enhanced security
274
287
  this.messageListener = (event) => {
288
+ var _a, _b, _c, _d, _e, _f, _g, _h;
275
289
  // Enhanced origin validation with strict allowlist
276
290
  if (!validatePostMessageOrigin(event, this.config.environment)) {
277
291
  logSecurityEvent('POSTMESSAGE_ORIGIN_BLOCKED', {
278
292
  origin: event.origin,
279
293
  environment: this.config.environment,
280
294
  expectedOrigins: `SafePassage trusted origins for ${this.config.environment}`,
281
- messageType: event.data?.type,
295
+ messageType: (_a = event.data) === null || _a === void 0 ? void 0 : _a.type,
282
296
  });
283
297
  return;
284
298
  }
@@ -289,7 +303,7 @@ export class SafePassage {
289
303
  error: messageValidation.error,
290
304
  origin: event.origin,
291
305
  sessionId: sessionId.substring(0, 8) + '...',
292
- messageType: event.data?.type,
306
+ messageType: (_b = event.data) === null || _b === void 0 ? void 0 : _b.type,
293
307
  });
294
308
  return;
295
309
  }
@@ -314,18 +328,19 @@ export class SafePassage {
314
328
  }
315
329
  // Trigger appropriate callback
316
330
  if (result.status === 'verified') {
317
- this.config.onComplete?.(result);
331
+ (_d = (_c = this.config).onComplete) === null || _d === void 0 ? void 0 : _d.call(_c, result);
318
332
  }
319
333
  else if (result.status === 'cancelled') {
320
- this.config.onCancel?.();
334
+ (_f = (_e = this.config).onCancel) === null || _f === void 0 ? void 0 : _f.call(_e);
321
335
  }
322
336
  else {
323
- this.config.onError?.(new Error(`Verification failed: ${result.status}`));
337
+ (_h = (_g = this.config).onError) === null || _h === void 0 ? void 0 : _h.call(_g, new Error(`Verification failed: ${result.status}`));
324
338
  }
325
339
  };
326
340
  window.addEventListener('message', this.messageListener);
327
341
  // Monitor popup window with proper cleanup
328
342
  this.popupMonitorInterval = setInterval(() => {
343
+ var _a, _b;
329
344
  if (this.popupWindow && this.popupWindow.closed) {
330
345
  // Log popup closed event
331
346
  logSecurityEvent('POPUP_CLOSED_BY_USER', {
@@ -336,7 +351,7 @@ export class SafePassage {
336
351
  this.cleanup();
337
352
  // Unlock verification after popup closed
338
353
  this.unlockVerification();
339
- this.config.onCancel?.();
354
+ (_b = (_a = this.config).onCancel) === null || _b === void 0 ? void 0 : _b.call(_a);
340
355
  }
341
356
  }, 500);
342
357
  }
@@ -556,6 +571,7 @@ export class SafePassage {
556
571
  * @private
557
572
  */
558
573
  async createInternalSession(options) {
574
+ var _a, _b;
559
575
  try {
560
576
  const portalApiUrl = this.getPortalApiUrl();
561
577
  const response = await fetch(`${portalApiUrl}/api/v1/sessions/create`, {
@@ -612,7 +628,7 @@ export class SafePassage {
612
628
  environment: this.config.environment,
613
629
  apiKeyType: 'public',
614
630
  });
615
- this.config.onError?.(error);
631
+ (_b = (_a = this.config).onError) === null || _b === void 0 ? void 0 : _b.call(_a, error);
616
632
  throw new Error(`Failed to create verification session: ${errorMessage}`);
617
633
  }
618
634
  }
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.0.15";
21
+ export declare const VERSION = "3.4.0";
package/dist/index.js CHANGED
@@ -24,8 +24,8 @@ if (typeof window !== 'undefined') {
24
24
  checkBrowserCompatibility();
25
25
  }
26
26
  export { SafePassage, SafePassage as default } from './core/SafePassageSDK';
27
- // Version - Updated for handoffToken QR code fix
28
- export const VERSION = '3.0.15';
27
+ // Version - 3.4.0: Deprecated cancelUrl (all redirects now use returnUrl with status param)
28
+ export const VERSION = '3.4.0';
29
29
  // For UMD builds
30
30
  if (typeof window !== 'undefined' && window) {
31
31
  // Dynamic import for UMD builds
@@ -1,3 +1,3 @@
1
1
  /* SafePassage SDK v3.0.15 - Redirect Implementation with Enhanced Security */
2
- "use strict";var SafePassageSDK=(()=>{var f=Object.defineProperty;var X=Object.getOwnPropertyDescriptor;var Y=Object.getOwnPropertyNames;var z=Object.prototype.hasOwnProperty;var p=(t,e)=>()=>(t&&(e=t(t=0)),e);var h=(t,e)=>{for(var n in e)f(t,n,{get:e[n],enumerable:!0})},Z=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of Y(e))!z.call(t,r)&&r!==n&&f(t,r,{get:()=>e[r],enumerable:!(i=X(e,r))||i.enumerable});return t};var A=t=>Z(f({},"__esModule",{value:!0}),t);function Q(t,e){return x[e].includes(t)}function V(t,e,n=[]){let{origin:i}=t;return Q(i,e)||n.length>0&&n.some(s=>{if(s.startsWith("*.")){let o=s.slice(2);return i.endsWith(`.${o}`)||i===`https://${o}`||i===`http://${o}`}return i===s})?!0:(console.warn(`SafePassage Security: Blocked PostMessage from untrusted origin: ${i}`,{environment:e,trustedOrigins:x[e],allowedCustomOrigins:n,eventType:t.data?.type}),!1)}function k(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","cancelled"].includes(n.status)?{isValid:!1,error:"Invalid status value"}:{isValid:!0}}function M(t){t==="production"&&window.location.protocol!=="https:"&&console.warn("SafePassage Warning: HTTPS recommended for production environment",{current:window.location.href})}function w(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{return{isValid:!1,error:"Invalid URL format"}}}function a(t,e){console.warn(`SafePassage Security Event: ${t}`,{timestamp:new Date().toISOString(),userAgent:navigator.userAgent,url:window.location.href,...e})}var x,m,L,v=p(()=>{"use strict";x={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"]};m=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)}},L=new m});var R={};h(R,{createSignedState:()=>te,generateHMAC:()=>S,generateSecureToken:()=>C,getSigningSecret:()=>y,parseSignedState:()=>ne,verifyHMAC:()=>_});async function S(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(l=>l.toString(16).padStart(2,"0")).join("")}async function _(t,e,n){try{let i=await S(t,n);return ee(e,i)}catch{return!1}}function ee(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 C(t=32){let e=new Uint8Array(t);return crypto.getRandomValues(e),Array.from(e,n=>n.toString(16).padStart(2,"0")).join("")}function y(t){return{production:"safepassage-prod-hmac-2025",staging:"safepassage-stage-hmac-2025"}[t]}async function te(t,e){let n={...t,timestamp:Date.now(),nonce:C(16)},i=JSON.stringify(n),r=y(e),s=await S(i,r);return btoa(JSON.stringify({data:n,signature:s}))}async function ne(t,e,n=$){try{let i=atob(t),r=JSON.parse(i);if(!r.data||!r.signature)return console.warn("SafePassage: Invalid signed state format"),null;let{data:s,signature:o}=r,l=JSON.stringify(s),u=y(e);if(!await _(l,o,u))return console.warn("SafePassage: State signature verification failed"),null;if(s.timestamp){let U=Date.now()-s.timestamp;if(U>n)return console.warn("SafePassage: State parameter expired",{age:U,maxAge:n}),null}let{timestamp:ce,nonce:le,...J}=s;return J}catch(i){return console.warn("SafePassage: Failed to parse signed state",i),null}}var D=p(()=>{"use strict";E()});function W(t){if(!t.apiKey)throw new Error("apiKey is required");if(t.apiKey.length>K)throw new Error(`apiKey exceeds maximum length of ${K} characters`);if(!ie.test(t.apiKey))throw new Error("Invalid apiKey format. Expected pk_xxx (public) or sk_xxx (private)");if(typeof window<"u"&&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>g)throw new Error(`returnUrl exceeds maximum length of ${g} characters`);if(!t.cancelUrl)throw new Error("cancelUrl is required");if(t.cancelUrl.length>g)throw new Error(`cancelUrl exceeds maximum length of ${g} characters`);let e=re(),n=w(t.returnUrl,e);if(!n.isValid)throw new Error(`returnUrl validation failed: ${n.error}`);let i=w(t.cancelUrl,e);if(!i.isValid)throw new Error(`cancelUrl validation failed: ${i.error}`);if(t.defaultChallengeAge!==void 0){if(t.defaultChallengeAge<O)throw new Error(`defaultChallengeAge must be at least ${O}`);if(t.defaultChallengeAge>N)throw new Error(`defaultChallengeAge cannot exceed ${N}`)}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 re(){if(typeof window>"u")return"production";let t=window.location.hostname;return t.includes("staging")||t.includes("stage")?"staging":"production"}async function H(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(D(),R));return n(t,e)}var O,N,g,K,$,ie,E=p(()=>{"use strict";v();O=25,N=150,g=2048,K=128,$=6e5,ie=/^(pk_|sk_)[a-zA-Z0-9_]+$/});function I(t){let e=q[t]||q.production;if(!e||!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function se(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 j(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{I(t),se(t)}catch(n){let i=n instanceof Error?n.message:String(n);throw new Error(`Environment configuration validation failed: ${i}`)}}var q,F=p(()=>{"use strict";q={production:"https://av.safepassageapp.com",staging:"https://av.staging.safepassageapp.com"}});var B={};h(B,{SafePassage:()=>c,default:()=>oe});var c,oe,P=p(()=>{"use strict";E();F();v();c=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;W(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={...e,environment:n,mode:e.mode||"redirect"},j(this.config.environment),M(this.config.environment),a("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={}){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 r=new Error(`Verification already in progress for session ${this.currentSessionId?.substring(0,8)}...`);throw a("RACE_CONDITION_PREVENTED",{currentSession:this.currentSessionId?.substring(0,8)+"...",attemptedSession:"new-session-attempt",origin:window.location.origin}),this.config.onError?.(r),r}this.isVerificationInProgress=!0,this.currentSessionId=i;try{let r=`${this.config.apiKey}:${window.location.origin}`;if(!L.isAllowed(r)){let o=new Error("Too many verification attempts. Please wait before trying again.");throw a("RATE_LIMIT_EXCEEDED",{apiKey:this.config.apiKey.substring(0,8)+"...",origin:window.location.origin,sessionId:i?i.substring(0,8)+"...":"undefined"}),this.config.onError?.(o),o}let s=await this.buildVerificationUrl(e,i);a("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(s,i):(this.unlockVerification(),this.redirect(s))}catch(r){throw this.unlockVerification(),r}}async buildVerificationUrl(e,n){let i=I(this.config.environment),r=e.challengeAge!==void 0,s=e.verificationMode!==void 0,o=r||s,l=await H({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 d=new URL(this.lastVerifyUrl);return d.searchParams.set("state",l),d.searchParams.set("mode",this.config.mode),d.toString()}catch{}let u=new URLSearchParams({state:l,sessionId:n,mode:this.config.mode});return`${i}/?${u.toString()}`}redirect(e){window.location.href=e}openNewTab(e,n){if(this.cleanup(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),this.popupWindow=window.open(e,"safepassage-verify","width=600,height=700"),!this.popupWindow){this.config.onError?.(new Error("Failed to open verification window. Please check popup blocker settings."));return}this.messageListener=i=>{if(!V(i,this.config.environment)){a("POSTMESSAGE_ORIGIN_BLOCKED",{origin:i.origin,environment:this.config.environment,expectedOrigins:`SafePassage trusted origins for ${this.config.environment}`,messageType:i.data?.type});return}let r=k(i,n);if(!r.isValid){a("POSTMESSAGE_VALIDATION_FAILED",{error:r.error,origin:i.origin,sessionId:n.substring(0,8)+"...",messageType:i.data?.type});return}let s={sessionId:i.data.sessionId,status:i.data.status};a("VERIFICATION_COMPLETED",{status:s.status,sessionId:n.substring(0,8)+"...",origin:i.origin}),this.cleanup(),this.unlockVerification(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),s.status==="verified"?this.config.onComplete?.(s):s.status==="cancelled"?this.config.onCancel?.():this.config.onError?.(new Error(`Verification failed: ${s.status}`))},window.addEventListener("message",this.messageListener),this.popupMonitorInterval=setInterval(()=>{this.popupWindow&&this.popupWindow.closed&&(a("POPUP_CLOSED_BY_USER",{sessionId:n.substring(0,8)+"...",environment:this.config.environment}),this.cleanup(),this.unlockVerification(),this.config.onCancel?.())},500)}setupAutoCleanup(){if(this.unloadListener=()=>{a("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,a("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(){a("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){try{let n=this.getPortalApiUrl(),i=await fetch(`${n}/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(!i.ok){let o=await i.json().catch(()=>({}));throw new Error(`Failed to create session: ${i.status} ${i.statusText}. ${o.message||""}`)}let r=await i.json(),s=r.sessionId;if(!s)throw new Error("Server did not return a sessionId");return r.verifyUrl&&(this.lastVerifyUrl=r.verifyUrl),r.sessionToken&&(this.lastSessionToken=r.sessionToken),r.handoffToken&&(this._temporaryHandoffToken=r.handoffToken),a("INTERNAL_SESSION_CREATED",{sessionId:s.substring(0,8)+"...",environment:this.config.environment,apiKeyType:"public"}),s}catch(n){let i=n instanceof Error?n.message:String(n);throw a("INTERNAL_SESSION_FAILED",{error:i,environment:this.config.environment,apiKeyType:"public"}),this.config.onError?.(n),new Error(`Failed to create verification session: ${i}`)}}},oe=c});var ae={};h(ae,{SafePassage:()=>c,VERSION:()=>G,default:()=>c});function b(){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 T(){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");try{if({a:{b:1}}?.a?.b!==1)throw new Error}catch{t.push("Optional chaining (?.) not supported, ensure transpilation for older browsers")}t.length>0&&console.warn("SafePassage SDK Browser Compatibility:",t.join("; "))}P();typeof window<"u"&&(b(),T());var G="3.0.15";if(typeof window<"u"&&window){let{SafePassage:t}=(P(),A(B)),e=window;e.SafePassage=t,e.SafePassage&&(e.SafePassage.VERSION=G)}return A(ae);})();
2
+ "use strict";var SafePassageSDK=(()=>{var S=Object.defineProperty,oe=Object.defineProperties,ae=Object.getOwnPropertyDescriptor,ce=Object.getOwnPropertyDescriptors,le=Object.getOwnPropertyNames,v=Object.getOwnPropertySymbols;var P=Object.prototype.hasOwnProperty,C=Object.prototype.propertyIsEnumerable;var R=(t,e,n)=>e in t?S(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,h=(t,e)=>{for(var n in e||(e={}))P.call(e,n)&&R(t,n,e[n]);if(v)for(var n of v(e))C.call(e,n)&&R(t,n,e[n]);return t},y=(t,e)=>oe(t,ce(e));var D=(t,e)=>{var n={};for(var i in t)P.call(t,i)&&e.indexOf(i)<0&&(n[i]=t[i]);if(t!=null&&v)for(var i of v(t))e.indexOf(i)<0&&C.call(t,i)&&(n[i]=t[i]);return n};var w=(t,e)=>()=>(t&&(e=t(t=0)),e);var U=(t,e)=>{for(var n in e)S(t,n,{get:e[n],enumerable:!0})},pe=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of le(e))!P.call(t,r)&&r!==n&&S(t,r,{get:()=>e[r],enumerable:!(i=ae(e,r))||i.enumerable});return t};var $=t=>pe(S({},"__esModule",{value:!0}),t);function de(t,e){return K[e].includes(t)}function W(t,e,n=[]){var r;let{origin:i}=t;return de(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:K[e],allowedCustomOrigins:n,eventType:(r=t.data)==null?void 0:r.type}),!1)}function H(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","cancelled"].includes(n.status)?{isValid:!1,error:"Invalid status value"}:{isValid:!0}}function j(t){t==="production"&&window.location.protocol!=="https:"&&console.warn("SafePassage Warning: HTTPS recommended for production environment",{current:window.location.href})}function T(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 K,A,q,b=w(()=>{"use strict";K={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"]};A=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)}},q=new A});var G={};U(G,{createSignedState:()=>ge,generateHMAC:()=>x,generateSecureToken:()=>B,getSigningSecret:()=>V,parseSignedState:()=>fe,verifyHMAC:()=>F});async function x(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 F(t,e,n){try{let i=await x(t,n);return ue(e,i)}catch(i){return!1}}function ue(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 B(t=32){let e=new Uint8Array(t);return crypto.getRandomValues(e),Array.from(e,n=>n.toString(16).padStart(2,"0")).join("")}function V(t){return{production:"safepassage-prod-hmac-2025",staging:"safepassage-stage-hmac-2025"}[t]}async function ge(t,e){let n=y(h({},t),{timestamp:Date.now(),nonce:B(16)}),i=JSON.stringify(n),r=V(e),s=await x(i,r);return btoa(JSON.stringify({data:n,signature:s}))}async function fe(t,e,n=X){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=V(e);if(!await F(p,a,c))return console.warn("SafePassage: State signature verification failed"),null;if(o.timestamp){let m=Date.now()-o.timestamp;if(m>n)return console.warn("SafePassage: State parameter expired",{age:m,maxAge:n}),null}let i=o,{timestamp:g,nonce:f}=i;return D(i,["timestamp","nonce"])}catch(r){return console.warn("SafePassage: Failed to parse signed state",r),null}}var J=w(()=>{"use strict";k()});function Q(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(!he.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>E)throw new Error(`returnUrl exceeds maximum length of ${E} characters`);let e=me(),n=T(t.returnUrl,e);if(!n.isValid)throw new Error(`returnUrl validation failed: ${n.error}`);if(t.cancelUrl){if(t.cancelUrl.length>E)throw new Error(`cancelUrl exceeds maximum length of ${E} characters`);let i=T(t.cancelUrl,e);if(!i.isValid)throw new Error(`cancelUrl validation failed: ${i.error}`)}if(t.defaultChallengeAge!==void 0){if(t.defaultChallengeAge<Y)throw new Error(`defaultChallengeAge must be at least ${Y}`);if(t.defaultChallengeAge>z)throw new Error(`defaultChallengeAge cannot exceed ${z}`)}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 me(){if(typeof window=="undefined")return"production";let t=window.location.hostname;return t.includes("staging")||t.includes("stage")?"staging":"production"}async function ee(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(J(),G));return n(t,e)}var Y,z,E,Z,X,he,k=w(()=>{"use strict";b();Y=25,z=150,E=2048,Z=128,X=6e5,he=/^(pk_|sk_)[a-zA-Z0-9_]+$/});function M(t){let e=te[t]||te.production;if(!e||!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function we(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 ne(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{M(t),we(t)}catch(n){let i=n instanceof Error?n.message:String(n);throw new Error(`Environment configuration validation failed: ${i}`)}}var te,ie=w(()=>{"use strict";te={production:"https://av.safepassageapp.com",staging:"https://av.staging.safepassageapp.com"}});var re={};U(re,{SafePassage:()=>u,default:()=>ve});var u,ve,_=w(()=>{"use strict";k();ie();b();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;Q(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=y(h({},e),{environment:n,mode:e.mode||"redirect"}),ne(this.config.environment),j(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(!q.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=M(this.config.environment),r=e.challengeAge!==void 0,s=e.verificationMode!==void 0,o=r||s,a=await ee({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,I,m,L;if(!W(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=H(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):a.status==="cancelled"?(I=(f=this.config).onCancel)==null||I.call(f):(L=(m=this.config).onError)==null||L.call(m,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}`)}}},ve=u});var Se={};U(Se,{SafePassage:()=>u,VERSION:()=>se,default:()=>u});function O(){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 N(){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("; "))}_();typeof window!="undefined"&&(O(),N());var se="3.4.0";if(typeof window!="undefined"&&window){let{SafePassage:t}=(_(),$(re)),e=window;e.SafePassage=t,e.SafePassage&&(e.SafePassage.VERSION=se)}return $(Se);})();
3
3
  if(typeof SafePassageSDK !== "undefined" && SafePassageSDK.SafePassage) { window.SafePassage = SafePassageSDK.SafePassage; window.SafePassage.VERSION = SafePassageSDK.VERSION; }
@@ -13,9 +13,10 @@ export interface SafePassageConfig {
13
13
  returnUrl: string;
14
14
  /**
15
15
  * URL to redirect to if user cancels verification
16
- * Must be pre-registered in dashboard
16
+ * @deprecated This field is no longer used. All redirects go to returnUrl with a status parameter.
17
+ * Kept for backwards compatibility but will be ignored.
17
18
  */
18
- cancelUrl: string;
19
+ cancelUrl?: string;
19
20
  /**
20
21
  * Environment to use
21
22
  * @default Auto-detected based on hostname (production unless staging detected)
@@ -68,6 +69,18 @@ export interface VerificationOptions {
68
69
  * Useful for correlating SafePassage sessions with merchant user records
69
70
  */
70
71
  externalUserId?: string;
72
+ /**
73
+ * Skip the intro screen and go directly to camera access
74
+ * Useful for embedded/streamlined flows where user has already consented
75
+ * @default false
76
+ */
77
+ skipIntro?: boolean;
78
+ /**
79
+ * Automatically redirect to returnUrl immediately after successful verification
80
+ * Skips the success screen for seamless embedded experiences
81
+ * @default false
82
+ */
83
+ autoReturn?: boolean;
71
84
  }
72
85
  export interface VerificationResult {
73
86
  /**
@@ -88,7 +101,8 @@ export interface StatePayload {
88
101
  merchantId: string;
89
102
  sessionId: string;
90
103
  returnUrl: string;
91
- cancelUrl: string;
104
+ /** @deprecated No longer used - all redirects use returnUrl with status parameter */
105
+ cancelUrl?: string;
92
106
  challengeAge?: number;
93
107
  verificationMode?: 'L1' | 'L2';
94
108
  hasOverrides?: boolean;
@@ -22,6 +22,17 @@
22
22
  * @author SafePassage Engineering
23
23
  * @version 1.0.0
24
24
  */
25
+ var __rest = (this && this.__rest) || function (s, e) {
26
+ var t = {};
27
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
28
+ t[p] = s[p];
29
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
30
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
31
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
32
+ t[p[i]] = s[p[i]];
33
+ }
34
+ return t;
35
+ };
25
36
  import { STATE_EXPIRY_MS } from './validation';
26
37
  /**
27
38
  * Generate HMAC-SHA256 signature using Web Crypto API
@@ -69,7 +80,7 @@ export async function verifyHMAC(data, signature, secret) {
69
80
  const expectedSignature = await generateHMAC(data, secret);
70
81
  return constantTimeCompare(signature, expectedSignature);
71
82
  }
72
- catch {
83
+ catch (_a) {
73
84
  return false;
74
85
  }
75
86
  }
@@ -145,11 +156,7 @@ export function getSigningSecret(environment) {
145
156
  */
146
157
  export async function createSignedState(payload, environment) {
147
158
  // Add timestamp for freshness
148
- const timestampedPayload = {
149
- ...payload,
150
- timestamp: Date.now(),
151
- nonce: generateSecureToken(16), // Add nonce to prevent replay attacks
152
- };
159
+ const timestampedPayload = Object.assign(Object.assign({}, payload), { timestamp: Date.now(), nonce: generateSecureToken(16) });
153
160
  const dataString = JSON.stringify(timestampedPayload);
154
161
  const secret = getSigningSecret(environment);
155
162
  const signature = await generateHMAC(dataString, secret);
@@ -199,7 +206,7 @@ export async function parseSignedState(signedState, environment, maxAge = STATE_
199
206
  }
200
207
  // Remove internal fields before returning
201
208
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
202
- const { timestamp, nonce, ...payload } = data;
209
+ const { timestamp, nonce } = data, payload = __rest(data, ["timestamp", "nonce"]);
203
210
  return payload;
204
211
  }
205
212
  catch (error) {
@@ -51,17 +51,6 @@ export function checkBrowserCompatibility() {
51
51
  if (!window.URLSearchParams) {
52
52
  warnings.push('URLSearchParams not supported, consider adding a polyfill for IE 11 support');
53
53
  }
54
- // Check for modern JavaScript features
55
- try {
56
- // Test optional chaining
57
- const test = { a: { b: 1 } };
58
- const result = test?.a?.b;
59
- if (result !== 1)
60
- throw new Error();
61
- }
62
- catch {
63
- warnings.push('Optional chaining (?.) not supported, ensure transpilation for older browsers');
64
- }
65
54
  // Log warnings if any
66
55
  if (warnings.length > 0) {
67
56
  console.warn('SafePassage SDK Browser Compatibility:', warnings.join('; '));
@@ -29,6 +29,7 @@ export function isOriginTrusted(origin, environment) {
29
29
  * Enhanced origin validation with logging and strict allowlist
30
30
  */
31
31
  export function validatePostMessageOrigin(event, environment, allowedCustomOrigins = []) {
32
+ var _a;
32
33
  const { origin } = event;
33
34
  // Check against trusted SafePassage origins
34
35
  if (isOriginTrusted(origin, environment)) {
@@ -55,7 +56,7 @@ export function validatePostMessageOrigin(event, environment, allowedCustomOrigi
55
56
  environment,
56
57
  trustedOrigins: TRUSTED_ORIGINS[environment],
57
58
  allowedCustomOrigins,
58
- eventType: event.data?.type,
59
+ eventType: (_a = event.data) === null || _a === void 0 ? void 0 : _a.type,
59
60
  });
60
61
  return false;
61
62
  }
@@ -125,7 +126,7 @@ export function validateReturnUrl(url, environment) {
125
126
  }
126
127
  return { isValid: true };
127
128
  }
128
- catch {
129
+ catch (_a) {
129
130
  return { isValid: false, error: 'Invalid URL format' };
130
131
  }
131
132
  }
@@ -185,11 +186,6 @@ export const verificationRateLimit = new VerificationRateLimit();
185
186
  * Security event logging for monitoring
186
187
  */
187
188
  export function logSecurityEvent(event, details) {
188
- console.warn(`SafePassage Security Event: ${event}`, {
189
- timestamp: new Date().toISOString(),
190
- userAgent: navigator.userAgent,
191
- url: window.location.href,
192
- ...details,
193
- });
189
+ console.warn(`SafePassage Security Event: ${event}`, Object.assign({ timestamp: new Date().toISOString(), userAgent: navigator.userAgent, url: window.location.href }, details));
194
190
  // In production, this could send events to a security monitoring service
195
191
  }
@@ -35,21 +35,21 @@ export function validateConfig(config) {
35
35
  if (config.returnUrl.length > MAX_URL_LENGTH) {
36
36
  throw new Error(`returnUrl exceeds maximum length of ${MAX_URL_LENGTH} characters`);
37
37
  }
38
- if (!config.cancelUrl) {
39
- throw new Error('cancelUrl is required');
40
- }
41
- if (config.cancelUrl.length > MAX_URL_LENGTH) {
42
- throw new Error(`cancelUrl exceeds maximum length of ${MAX_URL_LENGTH} characters`);
43
- }
44
38
  // Enhanced URL validation with security checks
45
39
  const environment = detectEnvironment();
46
40
  const returnUrlValidation = validateReturnUrl(config.returnUrl, environment);
47
41
  if (!returnUrlValidation.isValid) {
48
42
  throw new Error(`returnUrl validation failed: ${returnUrlValidation.error}`);
49
43
  }
50
- const cancelUrlValidation = validateReturnUrl(config.cancelUrl, environment);
51
- if (!cancelUrlValidation.isValid) {
52
- throw new Error(`cancelUrl validation failed: ${cancelUrlValidation.error}`);
44
+ // cancelUrl is deprecated but still validate if provided for backwards compatibility
45
+ if (config.cancelUrl) {
46
+ if (config.cancelUrl.length > MAX_URL_LENGTH) {
47
+ throw new Error(`cancelUrl exceeds maximum length of ${MAX_URL_LENGTH} characters`);
48
+ }
49
+ const cancelUrlValidation = validateReturnUrl(config.cancelUrl, environment);
50
+ if (!cancelUrlValidation.isValid) {
51
+ throw new Error(`cancelUrl validation failed: ${cancelUrlValidation.error}`);
52
+ }
53
53
  }
54
54
  if (config.defaultChallengeAge !== undefined) {
55
55
  if (config.defaultChallengeAge < MINIMUM_AGE) {
@@ -121,11 +121,10 @@ export async function parseState(state, environment) {
121
121
  if (signedPayload) {
122
122
  // Type guard to ensure signedPayload has required properties
123
123
  const payload = signedPayload;
124
- // Validate payload structure
124
+ // Validate payload structure (cancelUrl is deprecated and no longer required)
125
125
  if (!payload.merchantId ||
126
126
  !payload.sessionId ||
127
- !payload.returnUrl ||
128
- !payload.cancelUrl) {
127
+ !payload.returnUrl) {
129
128
  return null;
130
129
  }
131
130
  // Cast to unknown first to satisfy TypeScript's type checking
@@ -135,11 +134,10 @@ export async function parseState(state, environment) {
135
134
  console.warn('SafePassage: Falling back to legacy state format - update your SDK');
136
135
  const json = atob(state);
137
136
  const payload = JSON.parse(json);
138
- // Validate payload structure
137
+ // Validate payload structure (cancelUrl is deprecated and no longer required)
139
138
  if (!payload.merchantId ||
140
139
  !payload.sessionId ||
141
- !payload.returnUrl ||
142
- !payload.cancelUrl) {
140
+ !payload.returnUrl) {
143
141
  return null;
144
142
  }
145
143
  // Check timestamp expiration
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safepassage/sdk",
3
- "version": "3.2.4",
3
+ "version": "3.4.0",
4
4
  "description": "SafePassage SDK - Lightweight redirect-based age verification",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",