@safepassage/sdk 3.2.2 → 3.2.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.
@@ -47,6 +47,8 @@ export declare class SafePassage {
47
47
  private unloadListener;
48
48
  private isVerificationInProgress;
49
49
  private currentSessionId;
50
+ private lastVerifyUrl;
51
+ private lastSessionToken;
50
52
  /**
51
53
  * Initialize SafePassage SDK
52
54
  *
@@ -59,6 +59,10 @@ export class SafePassage {
59
59
  this.unloadListener = null;
60
60
  this.isVerificationInProgress = false;
61
61
  this.currentSessionId = null;
62
+ // Server-provided verify URL (includes sessionToken)
63
+ this.lastVerifyUrl = null;
64
+ // Server-provided session token (WS auth)
65
+ this.lastSessionToken = null;
62
66
  validateConfig(config);
63
67
  // Normalize environment: treat 'development' or any unknown value as 'production'
64
68
  let normalizedEnvironment = config.environment || this.detectEnvironment();
@@ -160,10 +164,8 @@ export class SafePassage {
160
164
  this.openNewTab(verificationUrl, sessionId);
161
165
  }
162
166
  else {
163
- // For redirect mode, navigate immediately so any page-level
164
- // loading indicator remains visible until unload. We intentionally
165
- // do not unlock here to avoid a brief flash of original content
166
- // before the browser leaves the page.
167
+ // For redirect mode, unlock immediately since we're leaving the page
168
+ this.unlockVerification();
167
169
  this.redirect(verificationUrl);
168
170
  }
169
171
  }
@@ -193,7 +195,6 @@ export class SafePassage {
193
195
  const state = await generateState({
194
196
  merchantId: this.config.apiKey,
195
197
  sessionId,
196
- sessionToken: this._sessionToken, // Include sessionToken for WebSocket auth
197
198
  returnUrl: this.config.returnUrl,
198
199
  cancelUrl: this.config.cancelUrl,
199
200
  challengeAge: options.challengeAge || this.config.defaultChallengeAge,
@@ -213,12 +214,23 @@ export class SafePassage {
213
214
  },
214
215
  // Include handoffToken if available (for QR code desktop flow)
215
216
  handoffToken: this._temporaryHandoffToken,
217
+ // Include sessionToken and verifyUrl to make UI auth deterministic
218
+ sessionToken: this.lastSessionToken || undefined,
219
+ verifyUrl: this.lastVerifyUrl || undefined,
216
220
  }, this.config.environment);
217
- const params = new URLSearchParams({
218
- state,
219
- sessionId,
220
- mode: this.config.mode,
221
- });
221
+ // Prefer server-provided verifyUrl (contains sessionToken) and append state/mode
222
+ if (this.lastVerifyUrl) {
223
+ try {
224
+ const url = new URL(this.lastVerifyUrl);
225
+ url.searchParams.set('state', state);
226
+ url.searchParams.set('mode', this.config.mode);
227
+ return url.toString();
228
+ }
229
+ catch {
230
+ // Fall back to client-constructed URL if parsing fails
231
+ }
232
+ }
233
+ const params = new URLSearchParams({ state, sessionId, mode: this.config.mode });
222
234
  return `${baseUrl}/?${params.toString()}`;
223
235
  }
224
236
  /**
@@ -572,12 +584,14 @@ export class SafePassage {
572
584
  if (!sessionId) {
573
585
  throw new Error('Server did not return a sessionId');
574
586
  }
575
- // Store the sessionToken for WebSocket authentication
576
- const sessionToken = sessionData.sessionToken;
577
- if (!sessionToken) {
578
- throw new Error('Server did not return a sessionToken');
587
+ // Capture verifyUrl from server for redirect (ensures sessionToken is present)
588
+ if (sessionData.verifyUrl) {
589
+ this.lastVerifyUrl = sessionData.verifyUrl;
590
+ }
591
+ // Capture sessionToken for state payload (ensures UI can always auth WS)
592
+ if (sessionData.sessionToken) {
593
+ this.lastSessionToken = sessionData.sessionToken;
579
594
  }
580
- this._sessionToken = sessionToken;
581
595
  // Store the handoffToken if it exists for desktop QR flow
582
596
  if (sessionData.handoffToken) {
583
597
  // Store it temporarily so it can be included in the state
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.2.1";
21
+ export declare const VERSION = "3.0.15";
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ if (typeof window !== 'undefined') {
25
25
  }
26
26
  export { SafePassage, SafePassage as default } from './core/SafePassageSDK';
27
27
  // Version - Updated for handoffToken QR code fix
28
- export const VERSION = '3.2.1';
28
+ export const VERSION = '3.0.15';
29
29
  // For UMD builds
30
30
  if (typeof window !== 'undefined' && window) {
31
31
  // Dynamic import for UMD builds
@@ -1,3 +1,3 @@
1
- /* SafePassage SDK v3.2.2 - Redirect Implementation with Enhanced Security */
2
- "use strict";var SafePassageSDK=(()=>{var u=Object.defineProperty;var J=Object.getOwnPropertyDescriptor;var X=Object.getOwnPropertyNames;var Y=Object.prototype.hasOwnProperty;var p=(t,e)=>()=>(t&&(e=t(t=0)),e);var f=(t,e)=>{for(var n in e)u(t,n,{get:e[n],enumerable:!0})},z=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of X(e))!Y.call(t,r)&&r!==n&&u(t,r,{get:()=>e[r],enumerable:!(i=J(e,r))||i.enumerable});return t};var U=t=>z(u({},"__esModule",{value:!0}),t);function Z(t,e){return T[e].includes(t)}function x(t,e,n=[]){let{origin:i}=t;return Z(i,e)||n.length>0&&n.some(o=>{if(o.startsWith("*.")){let s=o.slice(2);return i.endsWith(`.${s}`)||i===`https://${s}`||i===`http://${s}`}return i===o})?!0:(console.warn(`SafePassage Security: Blocked PostMessage from untrusted origin: ${i}`,{environment:e,trustedOrigins:T[e],allowedCustomOrigins:n,eventType:t.data?.type}),!1)}function V(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 k(t){t==="production"&&window.location.protocol!=="https:"&&console.warn("SafePassage Warning: HTTPS recommended for production environment",{current:window.location.href})}function m(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 T,h,M,w=p(()=>{"use strict";T={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"]};h=class{constructor(){this.attempts=new Map;this.maxAttempts=5;this.timeWindow=6e4}isAllowed(e){let n=Date.now(),r=(this.attempts.get(e)||[]).filter(o=>n-o<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)}},M=new h});var C={};f(C,{createSignedState:()=>ee,generateHMAC:()=>v,generateSecureToken:()=>_,getSigningSecret:()=>S,parseSignedState:()=>te,verifyHMAC:()=>L});async function v(t,e){let n=new TextEncoder,i=n.encode(e),r=n.encode(t),o=await crypto.subtle.importKey("raw",i,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),s=await crypto.subtle.sign("HMAC",o,r);return Array.from(new Uint8Array(s)).map(c=>c.toString(16).padStart(2,"0")).join("")}async function L(t,e,n){try{let i=await v(t,n);return Q(e,i)}catch{return!1}}function Q(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 _(t=32){let e=new Uint8Array(t);return crypto.getRandomValues(e),Array.from(e,n=>n.toString(16).padStart(2,"0")).join("")}function S(t){return{production:"safepassage-prod-hmac-2025",staging:"safepassage-stage-hmac-2025"}[t]}async function ee(t,e){let n={...t,timestamp:Date.now(),nonce:_(16)},i=JSON.stringify(n),r=S(e),o=await v(i,r);return btoa(JSON.stringify({data:n,signature:o}))}async function te(t,e,n=D){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:o,signature:s}=r,c=JSON.stringify(o),g=S(e);if(!await L(c,s,g))return console.warn("SafePassage: State signature verification failed"),null;if(o.timestamp){let P=Date.now()-o.timestamp;if(P>n)return console.warn("SafePassage: State parameter expired",{age:P,maxAge:n}),null}let{timestamp:ce,nonce:le,...G}=o;return G}catch(i){return console.warn("SafePassage: Failed to parse signed state",i),null}}var R=p(()=>{"use strict";y()});function K(t){if(!t.apiKey)throw new Error("apiKey is required");if(t.apiKey.length>N)throw new Error(`apiKey exceeds maximum length of ${N} characters`);if(!ne.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>d)throw new Error(`returnUrl exceeds maximum length of ${d} characters`);if(!t.cancelUrl)throw new Error("cancelUrl is required");if(t.cancelUrl.length>d)throw new Error(`cancelUrl exceeds maximum length of ${d} characters`);let e=ie(),n=m(t.returnUrl,e);if(!n.isValid)throw new Error(`returnUrl validation failed: ${n.error}`);let i=m(t.cancelUrl,e);if(!i.isValid)throw new Error(`cancelUrl validation failed: ${i.error}`);if(t.defaultChallengeAge!==void 0){if(t.defaultChallengeAge<$)throw new Error(`defaultChallengeAge must be at least ${$}`);if(t.defaultChallengeAge>O)throw new Error(`defaultChallengeAge cannot exceed ${O}`)}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 ie(){if(typeof window>"u")return"production";let t=window.location.hostname;return t.includes("staging")||t.includes("stage")?"staging":"production"}async function W(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(R(),C));return n(t,e)}var $,O,d,N,D,ne,y=p(()=>{"use strict";w();$=25,O=150,d=2048,N=128,D=6e5,ne=/^(pk_|sk_)[a-zA-Z0-9_]+$/});function E(t){let e=H[t]||H.production;if(!e||!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function re(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 q(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{E(t),re(t)}catch(n){let i=n instanceof Error?n.message:String(n);throw new Error(`Environment configuration validation failed: ${i}`)}}var H,j=p(()=>{"use strict";H={production:"https://av.safepassageapp.com",staging:"https://av.staging.safepassageapp.com"}});var F={};f(F,{SafePassage:()=>l,default:()=>oe});var l,oe,I=p(()=>{"use strict";y();j();w();l=class{constructor(e){this.popupWindow=null;this.messageListener=null;this.popupMonitorInterval=null;this.unloadListener=null;this.isVerificationInProgress=!1;this.currentSessionId=null;K(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"},q(this.config.environment),k(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(!M.isAllowed(r)){let s=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?.(s),s}let o=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(o,i):this.redirect(o)}catch(r){throw this.unlockVerification(),r}}async buildVerificationUrl(e,n){let i=E(this.config.environment),r=e.challengeAge!==void 0,o=e.verificationMode!==void 0,s=r||o,c=await W({merchantId:this.config.apiKey,sessionId:n,sessionToken:this._sessionToken,returnUrl:this.config.returnUrl,cancelUrl:this.config.cancelUrl,challengeAge:e.challengeAge||this.config.defaultChallengeAge,verificationMode:e.verificationMode||this.config.defaultVerificationMode,hasOverrides:s,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},this.config.environment),g=new URLSearchParams({state:c,sessionId:n,mode:this.config.mode});return`${i}/?${g.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(!x(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=V(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 o={sessionId:i.data.sessionId,status:i.data.status};a("VERIFICATION_COMPLETED",{status:o.status,sessionId:n.substring(0,8)+"...",origin:i.origin}),this.cleanup(),this.unlockVerification(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),o.status==="verified"?this.config.onComplete?.(o):o.status==="cancelled"?this.config.onCancel?.():this.config.onError?.(new Error(`Verification failed: ${o.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 c=await i.json().catch(()=>({}));throw new Error(`Failed to create session: ${i.status} ${i.statusText}. ${c.message||""}`)}let r=await i.json(),o=r.sessionId;if(!o)throw new Error("Server did not return a sessionId");let s=r.sessionToken;if(!s)throw new Error("Server did not return a sessionToken");return this._sessionToken=s,r.handoffToken&&(this._temporaryHandoffToken=r.handoffToken),a("INTERNAL_SESSION_CREATED",{sessionId:o.substring(0,8)+"...",environment:this.config.environment,apiKeyType:"public"}),o}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=l});var se={};f(se,{SafePassage:()=>l,VERSION:()=>B,default:()=>l});function A(){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 b(){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("; "))}I();typeof window<"u"&&(A(),b());var B="3.2.1";if(typeof window<"u"&&window){let{SafePassage:t}=(I(),U(F)),e=window;e.SafePassage=t,e.SafePassage&&(e.SafePassage.VERSION=B)}return U(se);})();
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);})();
3
3
  if(typeof SafePassageSDK !== "undefined" && SafePassageSDK.SafePassage) { window.SafePassage = SafePassageSDK.SafePassage; window.SafePassage.VERSION = SafePassageSDK.VERSION; }
@@ -89,7 +89,6 @@ export interface StatePayload {
89
89
  sessionId: string;
90
90
  returnUrl: string;
91
91
  cancelUrl: string;
92
- sessionToken?: string;
93
92
  challengeAge?: number;
94
93
  verificationMode?: 'L1' | 'L2';
95
94
  hasOverrides?: boolean;
@@ -105,6 +104,8 @@ export interface StatePayload {
105
104
  qualityThreshold: number;
106
105
  };
107
106
  handoffToken?: string;
107
+ sessionToken?: string;
108
+ verifyUrl?: string;
108
109
  }
109
110
  export interface SessionValidationResponse {
110
111
  sessionId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safepassage/sdk",
3
- "version": "3.2.2",
3
+ "version": "3.2.4",
4
4
  "description": "SafePassage SDK - Lightweight redirect-based age verification",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "repository": {
61
61
  "type": "git",
62
- "url": "git+https://github.com/safepassage/safepassage-monorepo.git",
62
+ "url": "https://github.com/safepassage/safepassage-monorepo",
63
63
  "directory": "services/verify-ui/sdk"
64
64
  },
65
65
  "bugs": {