@safepassage/sdk 3.0.15 → 3.1.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.
@@ -126,16 +126,17 @@ export declare class SafePassage {
126
126
  *
127
127
  * Analyzes the current hostname to determine the appropriate environment
128
128
  * configuration. Used when environment is not explicitly specified.
129
+ * Always defaults to production unless staging is detected.
129
130
  *
130
- * @returns {'production' | 'staging' | 'development'} Detected environment
131
+ * @returns {'production' | 'staging'} Detected environment
131
132
  * @private
132
133
  */
133
134
  private detectEnvironment;
134
135
  /**
135
136
  * Get the current environment
136
- * @returns {string} The current environment (production, staging, or development)
137
+ * @returns {string} The current environment (production or staging)
137
138
  */
138
- getEnvironment(): 'production' | 'staging' | 'development';
139
+ getEnvironment(): 'production' | 'staging';
139
140
  /**
140
141
  * Unlock verification process to allow new verifications
141
142
  *
@@ -361,17 +361,13 @@ export class SafePassage {
361
361
  *
362
362
  * Analyzes the current hostname to determine the appropriate environment
363
363
  * configuration. Used when environment is not explicitly specified.
364
+ * Always defaults to production unless staging is detected.
364
365
  *
365
- * @returns {'production' | 'staging' | 'development'} Detected environment
366
+ * @returns {'production' | 'staging'} Detected environment
366
367
  * @private
367
368
  */
368
369
  detectEnvironment() {
369
370
  const hostname = window.location.hostname;
370
- if (hostname === 'localhost' ||
371
- hostname === '127.0.0.1' ||
372
- hostname.includes('.local')) {
373
- return 'development';
374
- }
375
371
  if (hostname.includes('staging') || hostname.includes('stage')) {
376
372
  return 'staging';
377
373
  }
@@ -379,7 +375,7 @@ export class SafePassage {
379
375
  }
380
376
  /**
381
377
  * Get the current environment
382
- * @returns {string} The current environment (production, staging, or development)
378
+ * @returns {string} The current environment (production or staging)
383
379
  */
384
380
  getEnvironment() {
385
381
  return this.config.environment;
@@ -471,8 +467,6 @@ export class SafePassage {
471
467
  */
472
468
  getPortalApiUrl() {
473
469
  switch (this.config.environment) {
474
- case 'development':
475
- return 'http://localhost:3001';
476
470
  case 'staging':
477
471
  return 'https://api.staging.safepassageapp.com';
478
472
  case 'production':
@@ -492,8 +486,6 @@ export class SafePassage {
492
486
  */
493
487
  getEngineUrl() {
494
488
  switch (this.config.environment) {
495
- case 'development':
496
- return 'http://localhost:8000';
497
489
  case 'staging':
498
490
  return 'https://engine.staging.safepassageapp.com';
499
491
  case 'production':
@@ -514,8 +506,6 @@ export class SafePassage {
514
506
  */
515
507
  getWebSocketUrl() {
516
508
  switch (this.config.environment) {
517
- case 'development':
518
- return 'ws://localhost:8000/api/websocket/stream';
519
509
  case 'staging':
520
510
  return 'wss://engine.staging.safepassageapp.com/api/websocket/stream';
521
511
  case 'production':
@@ -1,3 +1,3 @@
1
- /* SafePassage SDK v3.0.14 - 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 g=(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 P=t=>z(u({},"__esModule",{value:!0}),t);function Z(t,e){return b[e].includes(t)}function T(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:b[e],allowedCustomOrigins:n,eventType:t.data?.type}),!1)}function x(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){if(t==="production"&&window.location.protocol!=="https:"){let e=window.location.href.replace("http:","https:");console.error("SafePassage Security: HTTPS required in production. Redirecting...",{current:window.location.href,redirect:e}),window.location.replace(e)}}function h(t,e){try{let n=new URL(t);if(e==="production"&&n.protocol!=="https:")return{isValid:!1,error:"HTTPS required for return URLs in production"};if(e==="development"&&!(n.hostname==="localhost"||n.hostname==="127.0.0.1"||n.hostname.endsWith(".local"))&&n.protocol!=="https:")return{isValid:!1,error:"Non-localhost URLs must use HTTPS"};if(e==="staging"&&n.protocol!=="https:")return{isValid:!1,error:"HTTPS required for return URLs in staging"};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 L(){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,M,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"],development:["http://localhost:5173","http://localhost:3000","http://localhost:3001","http://localhost:3002","http://127.0.0.1:5173","http://127.0.0.1:3000","http://127.0.0.1:3001","http://127.0.0.1:3002"]};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(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 f});var C={};g(C,{createSignedState:()=>ee,generateHMAC:()=>w,generateSecureToken:()=>R,getSigningSecret:()=>v,parseSignedState:()=>te,verifyHMAC:()=>k});async function w(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 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 R(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",development:"safepassage-dev-hmac-2025"}[t]}async function ee(t,e){let n={...t,timestamp:Date.now(),nonce:R(16)},i=JSON.stringify(n),r=v(e),o=await w(i,r);return btoa(JSON.stringify({data:n,signature:o}))}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:o,signature:s}=r,l=JSON.stringify(o),B=v(e);if(!await k(l,s,B))return console.warn("SafePassage: State signature verification failed"),null;if(o.timestamp){let E=Date.now()-o.timestamp;if(E>n)return console.warn("SafePassage: State parameter expired",{age:E,maxAge:n}),null}let{timestamp:le,nonce:pe,...G}=o;return G}catch(i){return console.warn("SafePassage: Failed to parse signed state",i),null}}var _=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==="localhost"||t==="127.0.0.1"||t.includes(".local")?"development":t.includes("staging")||t.includes("stage")?"staging":"production"}async function W(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(_(),C));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((t==="production"||t==="staging")&&!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function oe(t){let n={production:"https://api.safepassageapp.com",staging:"https://api-staging.safepassageapp.com",development:"http://localhost:3001"}[t];if((t==="production"||t==="staging")&&!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:",n=window.location.hostname;switch(t){case"production":if(!e)throw new Error("SafePassage requires HTTPS in production environment");break;case"staging":e||console.warn("SafePassage Warning: HTTPS strongly recommended in staging environment");break;case"development":{let i=n==="localhost"||n==="127.0.0.1"||n.includes(".local");!e&&!i&&console.warn("SafePassage Warning: HTTPS recommended for non-localhost development");break}}try{y(t),oe(t)}catch(i){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",development:"http://localhost:5173"}});var j={};g(j,{SafePassage:()=>c,default:()=>se});var c,se,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(!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,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(o,i):(this.unlockVerification(),this.redirect(o))}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,o=i||r,s=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: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},this.config.environment),l=new URLSearchParams({state:s,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(!T(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=x(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==="localhost"||e==="127.0.0.1"||e.includes(".local")?"development":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"development":return"http://localhost:3001";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"development":return"http://localhost:8000";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"development":return"ws://localhost:8000/api/websocket/stream";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=L();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 s=await r.json().catch(()=>({}));throw new Error(`Failed to create session: ${r.status} ${r.statusText}. ${s.message||""}`)}let o=await r.json();return o.handoffToken&&(this._temporaryHandoffToken=o.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}`)}}},se=c});var ae={};g(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);})();
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{if(new URL(t).protocol!=="https:")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,k,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)}},k=new f});var _={};u(_,{createSignedState:()=>ee,generateHMAC:()=>w,generateSecureToken:()=>C,getSigningSecret:()=>v,parseSignedState:()=>te,verifyHMAC:()=>L});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 L(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 L(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){throw new Error(`Environment configuration validation failed: ${n}`)}}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(!k.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);})();
3
3
  if(typeof SafePassageSDK !== "undefined" && SafePassageSDK.SafePassage) { window.SafePassage = SafePassageSDK.SafePassage; window.SafePassage.VERSION = SafePassageSDK.VERSION; }
@@ -18,9 +18,9 @@ export interface SafePassageConfig {
18
18
  cancelUrl: string;
19
19
  /**
20
20
  * Environment to use
21
- * @default Auto-detected based on hostname
21
+ * @default Auto-detected based on hostname (production unless staging detected)
22
22
  */
23
- environment?: 'production' | 'staging' | 'development';
23
+ environment?: 'production' | 'staging';
24
24
  /**
25
25
  * Verification mode
26
26
  * @default 'redirect'
@@ -103,7 +103,7 @@ export interface StatePayload {
103
103
  apiUrl?: string;
104
104
  engineUrl?: string;
105
105
  wsUrl?: string;
106
- environment?: 'production' | 'staging' | 'development';
106
+ environment?: 'production' | 'staging';
107
107
  features?: {
108
108
  testMode: boolean;
109
109
  warmupPeriodMs: number;
@@ -72,11 +72,11 @@ export declare function generateSecureToken(length?: number): string;
72
72
  * WARNING: Client-side secrets are NOT secure against determined attackers.
73
73
  * This is defense-in-depth only. Real security comes from server-side validation.
74
74
  *
75
- * @param {'production' | 'staging' | 'development'} environment - Target environment
75
+ * @param {'production' | 'staging'} environment - Target environment
76
76
  * @returns {string} Environment-specific secret
77
77
  * @export
78
78
  */
79
- export declare function getSigningSecret(environment: 'production' | 'staging' | 'development'): string;
79
+ export declare function getSigningSecret(environment: 'production' | 'staging'): string;
80
80
  /**
81
81
  * Create signed state parameter with timestamp and integrity protection
82
82
  *
@@ -85,11 +85,11 @@ export declare function getSigningSecret(environment: 'production' | 'staging' |
85
85
  * using HMAC-SHA256 and base64 encoded for URL safety.
86
86
  *
87
87
  * @param {unknown} payload - Data to include in state parameter
88
- * @param {'production' | 'staging' | 'development'} environment - Target environment
88
+ * @param {'production' | 'staging'} environment - Target environment
89
89
  * @returns {Promise<string>} Promise resolving to base64-encoded signed state
90
90
  * @export
91
91
  */
92
- export declare function createSignedState(payload: unknown, environment: 'production' | 'staging' | 'development'): Promise<string>;
92
+ export declare function createSignedState(payload: unknown, environment: 'production' | 'staging'): Promise<string>;
93
93
  /**
94
94
  * Verify and parse signed state parameter
95
95
  *
@@ -97,9 +97,9 @@ export declare function createSignedState(payload: unknown, environment: 'produc
97
97
  * and timestamp freshness. Returns the original payload if validation succeeds.
98
98
  *
99
99
  * @param {string} signedState - Base64-encoded signed state parameter
100
- * @param {'production' | 'staging' | 'development'} environment - Source environment
100
+ * @param {'production' | 'staging'} environment - Source environment
101
101
  * @param {number} [maxAge] - Maximum age in milliseconds
102
102
  * @returns {Promise<unknown | null>} Promise resolving to payload or null if invalid
103
103
  * @export
104
104
  */
105
- export declare function parseSignedState(signedState: string, environment: 'production' | 'staging' | 'development', maxAge?: number): Promise<unknown | null>;
105
+ export declare function parseSignedState(signedState: string, environment: 'production' | 'staging', maxAge?: number): Promise<unknown | null>;
@@ -118,7 +118,7 @@ export function generateSecureToken(length = 32) {
118
118
  * WARNING: Client-side secrets are NOT secure against determined attackers.
119
119
  * This is defense-in-depth only. Real security comes from server-side validation.
120
120
  *
121
- * @param {'production' | 'staging' | 'development'} environment - Target environment
121
+ * @param {'production' | 'staging'} environment - Target environment
122
122
  * @returns {string} Environment-specific secret
123
123
  * @export
124
124
  */
@@ -128,7 +128,6 @@ export function getSigningSecret(environment) {
128
128
  const baseSecrets = {
129
129
  production: 'safepassage-prod-hmac-2025',
130
130
  staging: 'safepassage-stage-hmac-2025',
131
- development: 'safepassage-dev-hmac-2025',
132
131
  };
133
132
  return baseSecrets[environment];
134
133
  }
@@ -140,7 +139,7 @@ export function getSigningSecret(environment) {
140
139
  * using HMAC-SHA256 and base64 encoded for URL safety.
141
140
  *
142
141
  * @param {unknown} payload - Data to include in state parameter
143
- * @param {'production' | 'staging' | 'development'} environment - Target environment
142
+ * @param {'production' | 'staging'} environment - Target environment
144
143
  * @returns {Promise<string>} Promise resolving to base64-encoded signed state
145
144
  * @export
146
145
  */
@@ -168,7 +167,7 @@ export async function createSignedState(payload, environment) {
168
167
  * and timestamp freshness. Returns the original payload if validation succeeds.
169
168
  *
170
169
  * @param {string} signedState - Base64-encoded signed state parameter
171
- * @param {'production' | 'staging' | 'development'} environment - Source environment
170
+ * @param {'production' | 'staging'} environment - Source environment
172
171
  * @param {number} [maxAge] - Maximum age in milliseconds
173
172
  * @returns {Promise<unknown | null>} Promise resolving to payload or null if invalid
174
173
  * @export
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Environment utilities for SafePassage SDK with security enforcement
3
3
  */
4
- export declare function getEnvironmentUrl(environment: 'production' | 'staging' | 'development'): string;
5
- export declare function getApiUrl(environment: 'production' | 'staging' | 'development'): string;
4
+ export declare function getEnvironmentUrl(environment: 'production' | 'staging'): string;
5
+ export declare function getApiUrl(environment: 'production' | 'staging'): string;
6
6
  /**
7
- * Detect environment with security warnings
7
+ * Detect environment - always defaults to production unless staging is detected
8
8
  */
9
- export declare function detectEnvironment(): 'production' | 'staging' | 'development';
9
+ export declare function detectEnvironment(): 'production' | 'staging';
10
10
  /**
11
11
  * Validate environment configuration on startup
12
12
  */
13
- export declare function validateEnvironmentSecurity(environment: 'production' | 'staging' | 'development'): void;
13
+ export declare function validateEnvironmentSecurity(environment: 'production' | 'staging'): void;
@@ -4,13 +4,11 @@
4
4
  const ENVIRONMENT_URLS = {
5
5
  production: 'https://av.safepassageapp.com',
6
6
  staging: 'https://av.staging.safepassageapp.com',
7
- development: 'http://localhost:5173',
8
7
  };
9
8
  export function getEnvironmentUrl(environment) {
10
9
  const url = ENVIRONMENT_URLS[environment];
11
10
  // Security check: Ensure production/staging always use HTTPS
12
- if ((environment === 'production' || environment === 'staging') &&
13
- !url.startsWith('https://')) {
11
+ if (!url.startsWith('https://')) {
14
12
  throw new Error(`HTTPS required for ${environment} environment`);
15
13
  }
16
14
  return url;
@@ -18,32 +16,20 @@ export function getEnvironmentUrl(environment) {
18
16
  export function getApiUrl(environment) {
19
17
  const apiUrls = {
20
18
  production: 'https://api.safepassageapp.com',
21
- staging: 'https://api-staging.safepassageapp.com',
22
- development: 'http://localhost:3001',
19
+ staging: 'https://api.staging.safepassageapp.com',
23
20
  };
24
21
  const url = apiUrls[environment];
25
- // Security check: Ensure production/staging always use HTTPS
26
- if ((environment === 'production' || environment === 'staging') &&
27
- !url.startsWith('https://')) {
22
+ // Security check: Ensure HTTPS is always used
23
+ if (!url.startsWith('https://')) {
28
24
  throw new Error(`HTTPS required for API URLs in ${environment} environment`);
29
25
  }
30
26
  return url;
31
27
  }
32
28
  /**
33
- * Detect environment with security warnings
29
+ * Detect environment - always defaults to production unless staging is detected
34
30
  */
35
31
  export function detectEnvironment() {
36
32
  const hostname = window.location.hostname;
37
- if (hostname === 'localhost' ||
38
- hostname === '127.0.0.1' ||
39
- hostname.includes('.local')) {
40
- // Warn if using HTTP in development with non-localhost domains
41
- if (window.location.protocol === 'http:' &&
42
- !hostname.match(/^(localhost|127\.0\.0\.1)$/)) {
43
- console.warn('SafePassage Security Warning: Using HTTP with non-localhost domain in development');
44
- }
45
- return 'development';
46
- }
47
33
  if (hostname.includes('staging') || hostname.includes('stage')) {
48
34
  // Enforce HTTPS in staging
49
35
  if (window.location.protocol !== 'https:') {
@@ -51,9 +37,9 @@ export function detectEnvironment() {
51
37
  }
52
38
  return 'staging';
53
39
  }
54
- // Production environment - strict HTTPS enforcement
40
+ // Default to production environment - strict HTTPS enforcement
55
41
  if (window.location.protocol !== 'https:') {
56
- console.error('SafePassage Security Error: HTTPS required in production environment');
42
+ console.warn('SafePassage Warning: HTTPS recommended for production environment');
57
43
  }
58
44
  return 'production';
59
45
  }
@@ -63,11 +49,10 @@ export function detectEnvironment() {
63
49
  export function validateEnvironmentSecurity(environment) {
64
50
  // Check current page protocol
65
51
  const isSecure = window.location.protocol === 'https:';
66
- const hostname = window.location.hostname;
67
52
  switch (environment) {
68
53
  case 'production':
69
54
  if (!isSecure) {
70
- throw new Error('SafePassage requires HTTPS in production environment');
55
+ console.warn('SafePassage Warning: HTTPS recommended for production environment');
71
56
  }
72
57
  break;
73
58
  case 'staging':
@@ -75,15 +60,6 @@ export function validateEnvironmentSecurity(environment) {
75
60
  console.warn('SafePassage Warning: HTTPS strongly recommended in staging environment');
76
61
  }
77
62
  break;
78
- case 'development': {
79
- const isLocalhost = hostname === 'localhost' ||
80
- hostname === '127.0.0.1' ||
81
- hostname.includes('.local');
82
- if (!isSecure && !isLocalhost) {
83
- console.warn('SafePassage Warning: HTTPS recommended for non-localhost development');
84
- }
85
- break;
86
- }
87
63
  }
88
64
  // Validate environment URLs
89
65
  try {
@@ -5,11 +5,11 @@
5
5
  /**
6
6
  * Validate if an origin is trusted for the given environment
7
7
  */
8
- export declare function isOriginTrusted(origin: string, environment: 'production' | 'staging' | 'development'): boolean;
8
+ export declare function isOriginTrusted(origin: string, environment: 'production' | 'staging'): boolean;
9
9
  /**
10
10
  * Enhanced origin validation with logging and strict allowlist
11
11
  */
12
- export declare function validatePostMessageOrigin(event: MessageEvent, environment: 'production' | 'staging' | 'development', allowedCustomOrigins?: string[]): boolean;
12
+ export declare function validatePostMessageOrigin(event: MessageEvent, environment: 'production' | 'staging', allowedCustomOrigins?: string[]): boolean;
13
13
  /**
14
14
  * Validate SafePassage message format and content
15
15
  */
@@ -20,11 +20,11 @@ export declare function validateSafePassageMessage(event: MessageEvent, expected
20
20
  /**
21
21
  * Enforce HTTPS in production environment
22
22
  */
23
- export declare function enforceHTTPS(environment: 'production' | 'staging' | 'development'): void;
23
+ export declare function enforceHTTPS(environment: 'production' | 'staging'): void;
24
24
  /**
25
25
  * Validate URL security for return/cancel URLs
26
26
  */
27
- export declare function validateReturnUrl(url: string, environment: 'production' | 'staging' | 'development'): {
27
+ export declare function validateReturnUrl(url: string, environment: 'production' | 'staging'): {
28
28
  isValid: boolean;
29
29
  error?: string;
30
30
  };
@@ -13,19 +13,9 @@ const TRUSTED_ORIGINS = {
13
13
  'https://api.safepassageapp.com',
14
14
  ],
15
15
  staging: [
16
- 'https://av-staging.safepassageapp.com',
17
- 'https://portal-staging.safepassageapp.com',
18
- 'https://api-staging.safepassageapp.com',
19
- ],
20
- development: [
21
- 'http://localhost:5173',
22
- 'http://localhost:3000',
23
- 'http://localhost:3001',
24
- 'http://localhost:3002',
25
- 'http://127.0.0.1:5173',
26
- 'http://127.0.0.1:3000',
27
- 'http://127.0.0.1:3001',
28
- 'http://127.0.0.1:3002',
16
+ 'https://av.staging.safepassageapp.com',
17
+ 'https://portal.staging.safepassageapp.com',
18
+ 'https://api.staging.safepassageapp.com',
29
19
  ],
30
20
  };
31
21
  /**
@@ -98,12 +88,9 @@ export function validateSafePassageMessage(event, expectedSessionId) {
98
88
  */
99
89
  export function enforceHTTPS(environment) {
100
90
  if (environment === 'production' && window.location.protocol !== 'https:') {
101
- const httpsUrl = window.location.href.replace('http:', 'https:');
102
- console.error('SafePassage Security: HTTPS required in production. Redirecting...', {
91
+ console.warn('SafePassage Warning: HTTPS recommended for production environment', {
103
92
  current: window.location.href,
104
- redirect: httpsUrl,
105
93
  });
106
- window.location.replace(httpsUrl);
107
94
  }
108
95
  }
109
96
  /**
@@ -112,27 +99,11 @@ export function enforceHTTPS(environment) {
112
99
  export function validateReturnUrl(url, environment) {
113
100
  try {
114
101
  const parsed = new URL(url);
115
- // Production must use HTTPS
116
- if (environment === 'production' && parsed.protocol !== 'https:') {
117
- return {
118
- isValid: false,
119
- error: 'HTTPS required for return URLs in production',
120
- };
121
- }
122
- // Development allows HTTP localhost
123
- if (environment === 'development') {
124
- const isLocalhost = parsed.hostname === 'localhost' ||
125
- parsed.hostname === '127.0.0.1' ||
126
- parsed.hostname.endsWith('.local');
127
- if (!isLocalhost && parsed.protocol !== 'https:') {
128
- return { isValid: false, error: 'Non-localhost URLs must use HTTPS' };
129
- }
130
- }
131
- // Staging should use HTTPS
132
- if (environment === 'staging' && parsed.protocol !== 'https:') {
102
+ // Production and staging should use HTTPS
103
+ if (parsed.protocol !== 'https:') {
133
104
  return {
134
105
  isValid: false,
135
- error: 'HTTPS required for return URLs in staging',
106
+ error: `HTTPS required for return URLs in ${environment}`,
136
107
  };
137
108
  }
138
109
  // Block suspicious URLs
@@ -14,8 +14,8 @@ export declare function validateChallengeAge(age?: number): void;
14
14
  * Generate signed state parameter with HMAC protection
15
15
  * Uses client-side HMAC for tamper resistance and server-side verification
16
16
  */
17
- export declare function generateState(payload: StatePayload, environment: 'production' | 'staging' | 'development'): Promise<string>;
17
+ export declare function generateState(payload: StatePayload, environment: 'production' | 'staging'): Promise<string>;
18
18
  /**
19
19
  * Parse and validate signed state parameter
20
20
  */
21
- export declare function parseState(state: string, environment: 'production' | 'staging' | 'development'): Promise<StatePayload | null>;
21
+ export declare function parseState(state: string, environment: 'production' | 'staging'): Promise<StatePayload | null>;
@@ -68,7 +68,7 @@ export function validateConfig(config) {
68
68
  }
69
69
  }
70
70
  /**
71
- * Detect environment based on current URL
71
+ * Detect environment based on current URL - defaults to production unless staging detected
72
72
  */
73
73
  function detectEnvironment() {
74
74
  // If no window (server environment), default to production for safety
@@ -76,11 +76,6 @@ function detectEnvironment() {
76
76
  return 'production';
77
77
  }
78
78
  const hostname = window.location.hostname;
79
- if (hostname === 'localhost' ||
80
- hostname === '127.0.0.1' ||
81
- hostname.includes('.local')) {
82
- return 'development';
83
- }
84
79
  if (hostname.includes('staging') || hostname.includes('stage')) {
85
80
  return 'staging';
86
81
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safepassage/sdk",
3
- "version": "3.0.15",
3
+ "version": "3.1.0",
4
4
  "description": "SafePassage SDK - Lightweight redirect-based age verification",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,4 +0,0 @@
1
- /**
2
- * Tests for SafePassage SDK
3
- */
4
- export {};
@@ -1,130 +0,0 @@
1
- /**
2
- * Tests for SafePassage SDK
3
- */
4
- import { SafePassage } from '../core/SafePassageSDK';
5
- describe('SafePassage SDK', () => {
6
- let mockLocation;
7
- let mockWindow;
8
- beforeEach(() => {
9
- // Mock window.location
10
- mockLocation = {
11
- href: 'https://merchant.com',
12
- hostname: 'merchant.com',
13
- origin: 'https://merchant.com'
14
- };
15
- // Mock window
16
- mockWindow = {
17
- location: mockLocation,
18
- open: jest.fn(),
19
- addEventListener: jest.fn(),
20
- removeEventListener: jest.fn()
21
- };
22
- // Replace global window
23
- global.window = mockWindow;
24
- });
25
- describe('constructor', () => {
26
- it('should initialize with valid config', () => {
27
- const config = {
28
- apiKey: 'sk_test_123',
29
- returnUrl: 'https://merchant.com/verified',
30
- cancelUrl: 'https://merchant.com/cancelled'
31
- };
32
- const sp = new SafePassage(config);
33
- expect(sp).toBeDefined();
34
- });
35
- it('should throw error for missing apiKey', () => {
36
- const config = {
37
- returnUrl: 'https://merchant.com/verified',
38
- cancelUrl: 'https://merchant.com/cancelled'
39
- };
40
- expect(() => new SafePassage(config)).toThrow('apiKey is required');
41
- });
42
- it('should throw error for invalid apiKey format', () => {
43
- const config = {
44
- apiKey: 'invalid_key',
45
- returnUrl: 'https://merchant.com/verified',
46
- cancelUrl: 'https://merchant.com/cancelled'
47
- };
48
- expect(() => new SafePassage(config)).toThrow('Invalid apiKey format');
49
- });
50
- it('should throw error for missing returnUrl', () => {
51
- const config = {
52
- apiKey: 'sk_test_123',
53
- cancelUrl: 'https://merchant.com/cancelled'
54
- };
55
- expect(() => new SafePassage(config)).toThrow('returnUrl is required');
56
- });
57
- it('should auto-detect development environment for localhost', () => {
58
- mockLocation.hostname = 'localhost';
59
- const config = {
60
- apiKey: 'sk_test_123',
61
- returnUrl: 'http://localhost:3000/verified',
62
- cancelUrl: 'http://localhost:3000/cancelled'
63
- };
64
- const sp = new SafePassage(config);
65
- expect(sp).toBeDefined();
66
- });
67
- it('should enforce minimum age of 25', () => {
68
- const config = {
69
- apiKey: 'sk_test_123',
70
- returnUrl: 'https://merchant.com/verified',
71
- cancelUrl: 'https://merchant.com/cancelled',
72
- defaultChallengeAge: 21
73
- };
74
- expect(() => new SafePassage(config)).toThrow('defaultChallengeAge must be at least 25');
75
- });
76
- });
77
- describe('verify', () => {
78
- let sp;
79
- beforeEach(() => {
80
- const config = {
81
- apiKey: 'sk_test_123',
82
- returnUrl: 'https://merchant.com/verified',
83
- cancelUrl: 'https://merchant.com/cancelled'
84
- };
85
- sp = new SafePassage(config);
86
- });
87
- it('should throw error for missing sessionId', () => {
88
- expect(() => sp.verify({})).toThrow('sessionId is required');
89
- });
90
- it('should redirect in same-tab mode', () => {
91
- const sessionId = '550e8400-e29b-41d4-a716-446655440000';
92
- sp.verify({ sessionId });
93
- expect(mockWindow.location.href).toContain('verify.safepassageapp.com');
94
- expect(mockWindow.location.href).toContain(`sessionId=${sessionId}`);
95
- });
96
- it('should open new tab in new-tab mode', () => {
97
- const config = {
98
- apiKey: 'sk_test_123',
99
- returnUrl: 'https://merchant.com/verified',
100
- cancelUrl: 'https://merchant.com/cancelled',
101
- mode: 'new-tab'
102
- };
103
- const sp = new SafePassage(config);
104
- const sessionId = '550e8400-e29b-41d4-a716-446655440000';
105
- sp.verify({ sessionId });
106
- expect(mockWindow.open).toHaveBeenCalledWith(expect.stringContaining('verify.safepassageapp.com'), 'safepassage-verify', 'width=600,height=700');
107
- });
108
- });
109
- describe('URL building', () => {
110
- it('should build correct URL with all parameters', () => {
111
- const config = {
112
- apiKey: 'sk_test_123',
113
- returnUrl: 'https://merchant.com/verified',
114
- cancelUrl: 'https://merchant.com/cancelled',
115
- environment: 'staging'
116
- };
117
- const sp = new SafePassage(config);
118
- // Access private method for testing
119
- const buildUrl = sp.buildVerificationUrl.bind(sp);
120
- const url = buildUrl({
121
- sessionId: '550e8400-e29b-41d4-a716-446655440000',
122
- challengeAge: 30,
123
- verificationMode: 'L2'
124
- });
125
- expect(url).toContain('verify-staging.safepassageapp.com');
126
- expect(url).toContain('sessionId=550e8400-e29b-41d4-a716-446655440000');
127
- expect(url).toContain('state=');
128
- });
129
- });
130
- });