@safepassage/sdk 3.1.3 → 3.2.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.
@@ -29,7 +29,7 @@
29
29
  */
30
30
  import { generateState, validateConfig } from '../utils/validation';
31
31
  import { getEnvironmentUrl, validateEnvironmentSecurity, } from '../utils/environment';
32
- import { validatePostMessageOrigin, validateSafePassageMessage, enforceHTTPS, verificationRateLimit, logSecurityEvent, generateSecureSessionId, } from '../utils/security';
32
+ import { validatePostMessageOrigin, validateSafePassageMessage, enforceHTTPS, verificationRateLimit, logSecurityEvent, } from '../utils/security';
33
33
  /**
34
34
  * SafePassage SDK Main Class
35
35
  *
@@ -60,9 +60,15 @@ export class SafePassage {
60
60
  this.isVerificationInProgress = false;
61
61
  this.currentSessionId = null;
62
62
  validateConfig(config);
63
+ // Normalize environment: treat 'development' or any unknown value as 'production'
64
+ let normalizedEnvironment = config.environment || this.detectEnvironment();
65
+ if (normalizedEnvironment !== 'staging' && normalizedEnvironment !== 'production') {
66
+ console.warn(`SafePassage SDK: Unknown environment '${normalizedEnvironment}', defaulting to 'production'`);
67
+ normalizedEnvironment = 'production';
68
+ }
63
69
  this.config = {
64
70
  ...config,
65
- environment: config.environment || this.detectEnvironment(),
71
+ environment: normalizedEnvironment,
66
72
  mode: config.mode || 'redirect',
67
73
  };
68
74
  // Comprehensive environment security validation
@@ -100,26 +106,25 @@ export class SafePassage {
100
106
  */
101
107
  async verify(options = {}) {
102
108
  const isPublicKey = this.isPublicKey();
103
- let sessionId = options.sessionId;
104
- // For public keys, generate session internally if not provided
105
- if (isPublicKey && !sessionId) {
109
+ // For public keys, create session via API (SafePassage generates the sessionId)
110
+ let sessionId;
111
+ if (isPublicKey) {
106
112
  sessionId = await this.createInternalSession(options);
107
113
  }
108
- // For private keys, sessionId is required
109
- if (!isPublicKey && !sessionId) {
110
- throw new Error('sessionId is required for private API keys - must be a merchant-generated UUID v4');
114
+ else {
115
+ // Private keys should use the direct API, not the SDK
116
+ throw new Error('Private API keys (sk_) should use the direct API, not the SDK. ' +
117
+ 'The SDK is designed for browser-based public key usage only.');
111
118
  }
112
119
  if (!sessionId) {
113
- throw new Error('Failed to create or obtain sessionId');
120
+ throw new Error('Failed to obtain sessionId from server');
114
121
  }
115
122
  // Race condition check - prevent multiple simultaneous verifications
116
123
  if (this.isVerificationInProgress) {
117
124
  const error = new Error(`Verification already in progress for session ${this.currentSessionId?.substring(0, 8)}...`);
118
125
  logSecurityEvent('RACE_CONDITION_PREVENTED', {
119
126
  currentSession: this.currentSessionId?.substring(0, 8) + '...',
120
- attemptedSession: options.sessionId
121
- ? options.sessionId.substring(0, 8) + '...'
122
- : 'undefined',
127
+ attemptedSession: 'new-session-attempt',
123
128
  origin: window.location.origin,
124
129
  });
125
130
  this.config.onError?.(error);
@@ -143,10 +148,7 @@ export class SafePassage {
143
148
  this.config.onError?.(error);
144
149
  throw error;
145
150
  }
146
- const verificationUrl = await this.buildVerificationUrl({
147
- ...options,
148
- sessionId,
149
- });
151
+ const verificationUrl = await this.buildVerificationUrl(options, sessionId);
150
152
  // Log verification attempt
151
153
  logSecurityEvent('VERIFICATION_INITIATED', {
152
154
  environment: this.config.environment,
@@ -180,7 +182,7 @@ export class SafePassage {
180
182
  * @returns {Promise<string>} Complete verification URL
181
183
  * @private
182
184
  */
183
- async buildVerificationUrl(options) {
185
+ async buildVerificationUrl(options, sessionId) {
184
186
  const baseUrl = getEnvironmentUrl(this.config.environment);
185
187
  // Determine if we have explicit overrides
186
188
  const hasExplicitChallengeAge = options.challengeAge !== undefined;
@@ -188,7 +190,7 @@ export class SafePassage {
188
190
  const hasOverrides = hasExplicitChallengeAge || hasExplicitVerificationMode;
189
191
  const state = await generateState({
190
192
  merchantId: this.config.apiKey,
191
- sessionId: options.sessionId,
193
+ sessionId,
192
194
  returnUrl: this.config.returnUrl,
193
195
  cancelUrl: this.config.cancelUrl,
194
196
  challengeAge: options.challengeAge || this.config.defaultChallengeAge,
@@ -211,7 +213,7 @@ export class SafePassage {
211
213
  }, this.config.environment);
212
214
  const params = new URLSearchParams({
213
215
  state,
214
- sessionId: options.sessionId,
216
+ sessionId,
215
217
  mode: this.config.mode,
216
218
  });
217
219
  return `${baseUrl}/?${params.toString()}`;
@@ -539,7 +541,6 @@ export class SafePassage {
539
541
  * @private
540
542
  */
541
543
  async createInternalSession(options) {
542
- const sessionId = generateSecureSessionId();
543
544
  try {
544
545
  const portalApiUrl = this.getPortalApiUrl();
545
546
  const response = await fetch(`${portalApiUrl}/api/v1/sessions/create`, {
@@ -550,7 +551,6 @@ export class SafePassage {
550
551
  },
551
552
  body: JSON.stringify({
552
553
  merchantId: this.config.apiKey, // API expects merchantId even though it uses auth header
553
- sessionId,
554
554
  returnUrl: this.config.returnUrl,
555
555
  cancelUrl: this.config.cancelUrl,
556
556
  challengeAge: options.challengeAge,
@@ -564,6 +564,11 @@ export class SafePassage {
564
564
  throw new Error(`Failed to create session: ${response.status} ${response.statusText}. ${errorData.message || ''}`);
565
565
  }
566
566
  const sessionData = await response.json();
567
+ // SafePassage now generates the sessionId internally
568
+ const sessionId = sessionData.sessionId;
569
+ if (!sessionId) {
570
+ throw new Error('Server did not return a sessionId');
571
+ }
567
572
  // Store the handoffToken if it exists for desktop QR flow
568
573
  if (sessionData.handoffToken) {
569
574
  // Store it temporarily so it can be included in the state
@@ -1,3 +1,3 @@
1
1
  /* SafePassage SDK v3.0.15 - Redirect Implementation with Enhanced Security */
2
- "use strict";var SafePassageSDK=(()=>{var g=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 u=(t,e)=>{for(var n in e)g(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&&g(t,r,{get:()=>e[r],enumerable:!(i=J(e,r))||i.enumerable});return t};var P=t=>z(g({},"__esModule",{value:!0}),t);function Z(t,e){return b[e].includes(t)}function x(t,e,n=[]){let{origin:i}=t;return Z(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:b[e],allowedCustomOrigins:n,eventType:t.data?.type}),!1)}function T(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 V(t){t==="production"&&window.location.protocol!=="https:"&&console.warn("SafePassage Warning: HTTPS recommended for production environment",{current:window.location.href})}function h(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 M(){if(crypto.randomUUID)return crypto.randomUUID();let t=new Uint8Array(16);crypto.getRandomValues(t);let e=Array.from(t).map(n=>n.toString(16).padStart(2,"0")).join("");return[e.slice(0,8),e.slice(8,12),"4"+e.slice(13,16),(parseInt(e.slice(16,17),16)&3|8).toString(16)+e.slice(17,20),e.slice(20,32)].join("-")}function a(t,e){console.warn(`SafePassage Security Event: ${t}`,{timestamp:new Date().toISOString(),userAgent:navigator.userAgent,url:window.location.href,...e})}var b,f,L,m=p(()=>{"use strict";b={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"]};f=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 f});var _={};u(_,{createSignedState:()=>ee,generateHMAC:()=>w,generateSecureToken:()=>C,getSigningSecret:()=>v,parseSignedState:()=>te,verifyHMAC:()=>k});async function w(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 k(t,e,n){try{let i=await w(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 C(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 ee(t,e){let n={...t,timestamp:Date.now(),nonce:C(16)},i=JSON.stringify(n),r=v(e),s=await w(i,r);return btoa(JSON.stringify({data:n,signature:s}))}async function te(t,e,n=O){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),B=v(e);if(!await k(l,o,B))return console.warn("SafePassage: State signature verification failed"),null;if(s.timestamp){let E=Date.now()-s.timestamp;if(E>n)return console.warn("SafePassage: State parameter expired",{age:E,maxAge:n}),null}let{timestamp:le,nonce:pe,...G}=s;return G}catch(i){return console.warn("SafePassage: Failed to parse signed state",i),null}}var R=p(()=>{"use strict";S()});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=h(t.returnUrl,e);if(!n.isValid)throw new Error(`returnUrl validation failed: ${n.error}`);let i=h(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>D)throw new Error(`defaultChallengeAge cannot exceed ${D}`)}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(),_));return n(t,e)}var $,D,d,N,O,ne,S=p(()=>{"use strict";m();$=25,D=150,d=2048,N=128,O=6e5,ne=/^(pk_|sk_)[a-zA-Z0-9_]+$/});function y(t){let e=re[t];if(!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function se(t){let n={production:"https://api.safepassageapp.com",staging:"https://api.staging.safepassageapp.com"}[t];if(!n.startsWith("https://"))throw new Error(`HTTPS required for API URLs in ${t} environment`);return n}function H(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{y(t),se(t)}catch(n){let i=n instanceof Error?n.message:String(n);throw new Error(`Environment configuration validation failed: ${i}`)}}var re,q=p(()=>{"use strict";re={production:"https://av.safepassageapp.com",staging:"https://av.staging.safepassageapp.com"}});var j={};u(j,{SafePassage:()=>c,default:()=>oe});var c,oe,I=p(()=>{"use strict";S();q();m();c=class{constructor(e){this.popupWindow=null;this.messageListener=null;this.popupMonitorInterval=null;this.unloadListener=null;this.isVerificationInProgress=!1;this.currentSessionId=null;K(e),this.config={...e,environment:e.environment||this.detectEnvironment(),mode:e.mode||"redirect"},H(this.config.environment),V(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=e.sessionId;if(n&&!i&&(i=await this.createInternalSession(e)),!n&&!i)throw new Error("sessionId is required for private API keys - must be a merchant-generated UUID v4");if(!i)throw new Error("Failed to create or obtain sessionId");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:e.sessionId?e.sessionId.substring(0,8)+"...":"undefined",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,sessionId: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){let n=y(this.config.environment),i=e.challengeAge!==void 0,r=e.verificationMode!==void 0,s=i||r,o=await W({merchantId:this.config.apiKey,sessionId:e.sessionId,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),l=new URLSearchParams({state:o,sessionId:e.sessionId,mode:this.config.mode});return`${n}/?${l.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=T(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){let n=M();try{let i=this.getPortalApiUrl(),r=await fetch(`${i}/api/v1/sessions/create`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({merchantId:this.config.apiKey,sessionId:n,returnUrl:this.config.returnUrl,cancelUrl:this.config.cancelUrl,challengeAge:e.challengeAge,verificationMode:e.verificationMode,merchantName:document.title||window.location.hostname,externalUserId:e.externalUserId})});if(!r.ok){let o=await r.json().catch(()=>({}));throw new Error(`Failed to create session: ${r.status} ${r.statusText}. ${o.message||""}`)}let s=await r.json();return s.handoffToken&&(this._temporaryHandoffToken=s.handoffToken),a("INTERNAL_SESSION_CREATED",{sessionId:n.substring(0,8)+"...",environment:this.config.environment,apiKeyType:"public"}),n}catch(i){let r=i instanceof Error?i.message:String(i);throw a("INTERNAL_SESSION_FAILED",{error:r,environment:this.config.environment,apiKeyType:"public"}),this.config.onError?.(i),new Error(`Failed to create verification session: ${r}`)}}},oe=c});var ae={};u(ae,{SafePassage:()=>c,VERSION:()=>F,default:()=>c});function U(){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 A(){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"&&(U(),A());var F="3.0.15";if(typeof window<"u"&&window){let{SafePassage:t}=(I(),P(j)),e=window;e.SafePassage=t,e.SafePassage&&(e.SafePassage.VERSION=F)}return P(ae);})();
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 M(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,k,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)}},k=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(l=>l.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,l=JSON.stringify(o),g=S(e);if(!await L(l,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:()=>c,default:()=>oe});var c,oe,I=p(()=>{"use strict";y();j();w();c=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),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(!k.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.unlockVerification(),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,l=await W({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: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:l,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 s=await i.json().catch(()=>({}));throw new Error(`Failed to create session: ${i.status} ${i.statusText}. ${s.message||""}`)}let r=await i.json(),o=r.sessionId;if(!o)throw new Error("Server did not return a sessionId");return 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=c});var se={};f(se,{SafePassage:()=>c,VERSION:()=>B,default:()=>c});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.0.15";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);})();
3
3
  if(typeof SafePassageSDK !== "undefined" && SafePassageSDK.SafePassage) { window.SafePassage = SafePassageSDK.SafePassage; window.SafePassage.VERSION = SafePassageSDK.VERSION; }
@@ -50,12 +50,6 @@ export interface SafePassageConfig {
50
50
  onError?: (error: Error) => void;
51
51
  }
52
52
  export interface VerificationOptions {
53
- /**
54
- * Merchant-generated UUID v4 for this verification session
55
- * Required for private keys (sk_), optional for public keys (pk_)
56
- * For public keys: SDK will generate session internally
57
- */
58
- sessionId?: string;
59
53
  /**
60
54
  * Minimum age to verify (minimum 25)
61
55
  * @default Uses merchant dashboard configuration
@@ -129,7 +123,6 @@ export interface SessionCreationResponse {
129
123
  expiresAt: string;
130
124
  }
131
125
  export interface CreateSessionRequest {
132
- sessionId: string;
133
126
  returnUrl: string;
134
127
  cancelUrl?: string;
135
128
  challengeAge?: number;
@@ -6,9 +6,10 @@ const ENVIRONMENT_URLS = {
6
6
  staging: 'https://av.staging.safepassageapp.com',
7
7
  };
8
8
  export function getEnvironmentUrl(environment) {
9
- const url = ENVIRONMENT_URLS[environment];
9
+ // Default to production for unknown environments (like 'development')
10
+ const url = ENVIRONMENT_URLS[environment] || ENVIRONMENT_URLS.production;
10
11
  // Security check: Ensure production/staging always use HTTPS
11
- if (!url.startsWith('https://')) {
12
+ if (!url || !url.startsWith('https://')) {
12
13
  throw new Error(`HTTPS required for ${environment} environment`);
13
14
  }
14
15
  return url;
@@ -18,9 +19,10 @@ export function getApiUrl(environment) {
18
19
  production: 'https://api.safepassageapp.com',
19
20
  staging: 'https://api.staging.safepassageapp.com',
20
21
  };
21
- const url = apiUrls[environment];
22
+ // Default to production for unknown environments (like 'development')
23
+ const url = apiUrls[environment] || apiUrls.production;
22
24
  // Security check: Ensure HTTPS is always used
23
- if (!url.startsWith('https://')) {
25
+ if (!url || !url.startsWith('https://')) {
24
26
  throw new Error(`HTTPS required for API URLs in ${environment} environment`);
25
27
  }
26
28
  return url;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safepassage/sdk",
3
- "version": "3.1.3",
3
+ "version": "3.2.0",
4
4
  "description": "SafePassage SDK - Lightweight redirect-based age verification",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",