@safepassage/sdk 3.0.4 → 3.0.6

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.
@@ -1,3 +1,3 @@
1
- /* SafePassage SDK v3.0.4 - Redirect Implementation with Enhanced Security */
2
- "use strict";var SafePassageSDK=(()=>{var u=Object.defineProperty;var G=Object.getOwnPropertyDescriptor;var J=Object.getOwnPropertyNames;var X=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})},Y=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of J(e))!X.call(t,r)&&r!==n&&u(t,r,{get:()=>e[r],enumerable:!(i=G(e,r))||i.enumerable});return t};var P=t=>Y(u({},"__esModule",{value:!0}),t);function z(t,e){return T[e].includes(t)}function b(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 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 a(t,e){console.warn(`SafePassage Security Event: ${t}`,{timestamp:new Date().toISOString(),userAgent:navigator.userAgent,url:window.location.href,...e})}var T,f,L,m=p(()=>{"use strict";T={production:["https://verify.safepassageapp.com","https://portal.safepassageapp.com","https://api.safepassageapp.com"],staging:["https://verify-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)}},L=new f});var R={};g(R,{createSignedState:()=>Q,generateHMAC:()=>w,generateSecureToken:()=>M,getSigningSecret:()=>v,parseSignedState:()=>ee,verifyHMAC:()=>C});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 C(t,e,n){try{let i=await w(t,n);return Z(e,i)}catch{return!1}}function Z(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 M(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 Q(t,e){let n={...t,timestamp:Date.now(),nonce:M(16)},i=JSON.stringify(n),r=v(e),o=await w(i,r);return btoa(JSON.stringify({data:n,signature:o}))}async function ee(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),F=v(e);if(!await C(l,s,F))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:ce,nonce:le,...B}=o;return B}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(!te.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=ne(),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>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 ne(){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 K(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(_(),R));return n(t,e)}var $,O,d,N,D,te,S=p(()=>{"use strict";m();$=25,O=150,d=2048,N=128,D=6e5,te=/^(pk_|sk_)[a-zA-Z0-9]+$/});function y(t){let e=ie[t];if((t==="production"||t==="staging")&&!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function re(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 W(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),re(t)}catch(i){throw new Error(`Environment configuration validation failed: ${i}`)}}var ie,H=p(()=>{"use strict";ie={production:"https://verify.safepassageapp.com",staging:"https://verify-staging.safepassageapp.com",development:"http://localhost:5173"}});var q={};g(q,{SafePassage:()=>c,default:()=>oe});var c,oe,I=p(()=>{"use strict";S();H();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"},W(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 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 K({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()},this.config.environment),l=new URLSearchParams({state:s,sessionId:e.sessionId,mode:this.config.mode});return`${n}/verify?${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(!b(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"}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()}isPublicKey(){return this.config.apiKey.startsWith("pk_")}async createInternalSession(e){let n=crypto.randomUUID();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 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}`)}}getPortalApiUrl(){switch(this.config.environment){case"production":return"https://api.safepassageapp.com";case"staging":return"https://api-staging.safepassageapp.com";case"development":default:return"http://localhost:3001"}}},oe=c});var se={};g(se,{SafePassage:()=>c,VERSION:()=>j,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 j="3.0.4";if(typeof window<"u"&&window){let{SafePassage:t}=(I(),P(q));window.SafePassage=t,window.SafePassage.VERSION=j}return P(se);})();
1
+ /* SafePassage SDK v3.0.6 - Redirect Implementation with Enhanced Security */
2
+ "use strict";var SafePassageSDK=(()=>{var u=Object.defineProperty;var G=Object.getOwnPropertyDescriptor;var J=Object.getOwnPropertyNames;var X=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})},Y=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of J(e))!X.call(t,r)&&r!==n&&u(t,r,{get:()=>e[r],enumerable:!(i=G(e,r))||i.enumerable});return t};var P=t=>Y(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 a=o.slice(2);return i.endsWith(`.${a}`)||i===`https://${a}`||i===`http://${a}`}return i===o})?!0:(console.warn(`SafePassage Security: Blocked PostMessage from untrusted origin: ${i}`,{environment:e,trustedOrigins: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 s(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.safepassageapp.com","https://portal.safepassageapp.com","https://api.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)}},L=new f});var C={};g(C,{createSignedState:()=>Q,generateHMAC:()=>w,generateSecureToken:()=>R,getSigningSecret:()=>v,parseSignedState:()=>ee,verifyHMAC:()=>M});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"]),a=await crypto.subtle.sign("HMAC",o,r);return Array.from(new Uint8Array(a)).map(l=>l.toString(16).padStart(2,"0")).join("")}async function M(t,e,n){try{let i=await w(t,n);return Z(e,i)}catch{return!1}}function Z(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-prod-hmac-2025",development:"safepassage-dev-hmac-2025"}[t]}async function Q(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 ee(t,e,n=k){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:a}=r,l=JSON.stringify(o),F=v(e);if(!await M(l,a,F))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:ce,nonce:le,...B}=o;return B}catch(i){return console.warn("SafePassage: Failed to parse signed state",i),null}}var _=p(()=>{"use strict";S()});function N(t){if(!t.apiKey)throw new Error("apiKey is required");if(t.apiKey.length>D)throw new Error(`apiKey exceeds maximum length of ${D} characters`);if(!te.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=ne(),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<O)throw new Error(`defaultChallengeAge must be at least ${O}`);if(t.defaultChallengeAge>$)throw new Error(`defaultChallengeAge cannot exceed ${$}`)}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 ne(){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 K(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(_(),C));return n(t,e)}var O,$,d,D,k,te,S=p(()=>{"use strict";m();O=25,$=150,d=2048,D=128,k=6e5,te=/^(pk_|sk_)[a-zA-Z0-9]+$/});function y(t){let e=ie[t];if((t==="production"||t==="staging")&&!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function re(t){let n={production:"https://api.safepassageapp.com",staging:"https://api.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 W(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),re(t)}catch(i){throw new Error(`Environment configuration validation failed: ${i}`)}}var ie,H=p(()=>{"use strict";ie={production:"https://av.safepassageapp.com",staging:"https://av.safepassageapp.com",development:"http://localhost:5173"}});var q={};g(q,{SafePassage:()=>c,default:()=>oe});var c,oe,I=p(()=>{"use strict";S();H();m();c=class{constructor(e){this.popupWindow=null;this.messageListener=null;this.popupMonitorInterval=null;this.unloadListener=null;this.isVerificationInProgress=!1;this.currentSessionId=null;N(e),this.config={...e,environment:e.environment||this.detectEnvironment(),mode:e.mode||"redirect"},W(this.config.environment),V(this.config.environment),s("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 s("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 a=new Error("Too many verification attempts. Please wait before trying again.");throw s("RATE_LIMIT_EXCEEDED",{apiKey:this.config.apiKey.substring(0,8)+"...",origin:window.location.origin,sessionId:i?i.substring(0,8)+"...":"undefined"}),this.config.onError?.(a),a}let o=await this.buildVerificationUrl({...e,sessionId:i});s("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,a=await K({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:{captureMode:"enhanced_verification",testMode:!1,warmupPeriodMs:2e3,qualityThreshold:.6}},this.config.environment),l=new URLSearchParams({state:a,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)){s("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){s("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};s("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&&(s("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=()=>{s("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"}unlockVerification(){this.isVerificationInProgress=!1,this.currentSessionId=null,s("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(){s("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":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":case"production":return"https://engine.safepassageapp.com";default:return"https://engine.safepassageapp.com"}}getWebSocketUrl(){switch(this.config.environment){case"development":return"ws://localhost:8000/safe-passage-llm/api/v1/websocket/stream";case"staging":case"production":return"wss://engine.safepassageapp.com/safe-passage-llm/api/v1/websocket/stream";default:return"wss://engine.safepassageapp.com/safe-passage-llm/api/v1/websocket/stream"}}isPublicKey(){return this.config.apiKey.startsWith("pk_")}async createInternalSession(e){let n=crypto.randomUUID();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||""}`)}return await r.json(),s("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 s("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 se={};g(se,{SafePassage:()=>c,VERSION:()=>j,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 j="3.0.6";if(typeof window<"u"&&window){let{SafePassage:t}=(I(),P(q)),e=window;e.SafePassage=t,e.SafePassage&&(e.SafePassage.VERSION=j)}return P(se);})();
3
3
  if(typeof SafePassageSDK !== "undefined" && SafePassageSDK.SafePassage) { window.SafePassage = SafePassageSDK.SafePassage; window.SafePassage.VERSION = SafePassageSDK.VERSION; }
@@ -100,6 +100,16 @@ export interface StatePayload {
100
100
  hasOverrides?: boolean;
101
101
  externalUserId?: string;
102
102
  timestamp: number;
103
+ apiUrl?: string;
104
+ engineUrl?: string;
105
+ wsUrl?: string;
106
+ environment?: 'production' | 'staging' | 'development';
107
+ features?: {
108
+ captureMode: 'basic_verification' | 'enhanced_verification';
109
+ testMode: boolean;
110
+ warmupPeriodMs: number;
111
+ qualityThreshold: number;
112
+ };
103
113
  }
104
114
  export interface SessionValidationResponse {
105
115
  sessionId: string;
@@ -0,0 +1,3 @@
1
+ export declare function setupPolyfills(): void;
2
+ export declare function checkBrowserCompatibility(): void;
3
+ export declare function polyfillCryptoRandomUUID(): void;
@@ -0,0 +1,10 @@
1
+ // Mock polyfills to avoid browser API checks in tests
2
+ export function setupPolyfills() {
3
+ // Do nothing in tests
4
+ }
5
+ export function checkBrowserCompatibility() {
6
+ // Do nothing in tests
7
+ }
8
+ export function polyfillCryptoRandomUUID() {
9
+ // Already mocked in test setup
10
+ }
@@ -1,31 +1,105 @@
1
1
  /**
2
2
  * Crypto utilities for SafePassage SDK - Browser compatible
3
+ *
4
+ * Provides cryptographic functions for the SafePassage SDK using Web Crypto API.
5
+ * These utilities handle HMAC signing, state parameter protection, and secure
6
+ * token generation for client-side security measures.
7
+ *
8
+ * IMPORTANT SECURITY NOTE:
9
+ * Client-side cryptography provides defense-in-depth but cannot be considered
10
+ * secure against determined attackers. True security comes from server-side
11
+ * validation. These functions are primarily for tamper resistance and
12
+ * integrity checking.
13
+ *
14
+ * Features:
15
+ * - HMAC-SHA256 signing and verification using Web Crypto API
16
+ * - Constant-time string comparison to prevent timing attacks
17
+ * - Secure random token generation
18
+ * - Signed state parameter creation and parsing
19
+ * - Environment-specific secret derivation
20
+ * - Timestamp and nonce-based replay protection
21
+ *
22
+ * @author SafePassage Engineering
23
+ * @version 1.0.0
3
24
  */
4
25
  /**
5
26
  * Generate HMAC-SHA256 signature using Web Crypto API
6
- * Note: This is client-side HMAC, the secret is known to the client.
7
- * For true security, verification should still happen server-side.
27
+ *
28
+ * Creates an HMAC-SHA256 signature for the given data using the provided secret.
29
+ * Uses the browser's Web Crypto API for cryptographic operations.
30
+ *
31
+ * SECURITY NOTE: This is client-side HMAC where the secret is known to the client.
32
+ * It provides tamper resistance but not true security. Server-side verification
33
+ * is required for actual security.
34
+ *
35
+ * @param {string} data - Data to sign
36
+ * @param {string} secret - HMAC secret key
37
+ * @returns {Promise<string>} Promise resolving to hex-encoded HMAC signature
38
+ * @export
8
39
  */
9
40
  export declare function generateHMAC(data: string, secret: string): Promise<string>;
10
41
  /**
11
42
  * Verify HMAC-SHA256 signature using Web Crypto API
43
+ *
44
+ * Verifies an HMAC-SHA256 signature against the provided data and secret.
45
+ * Uses constant-time comparison to prevent timing attacks.
46
+ *
47
+ * @param {string} data - Original data that was signed
48
+ * @param {string} signature - HMAC signature to verify
49
+ * @param {string} secret - HMAC secret key
50
+ * @returns {Promise<boolean>} Promise resolving to true if signature is valid
51
+ * @export
12
52
  */
13
53
  export declare function verifyHMAC(data: string, signature: string, secret: string): Promise<boolean>;
14
54
  /**
15
55
  * Generate a secure random token for client-side use
56
+ *
57
+ * Creates a cryptographically secure random token using the browser's
58
+ * crypto.getRandomValues() function.
59
+ *
60
+ * @param {number} [length=32] - Token length in bytes
61
+ * @returns {string} Hex-encoded random token
62
+ * @export
16
63
  */
17
64
  export declare function generateSecureToken(length?: number): string;
18
65
  /**
19
66
  * Environment-specific secret derivation
20
- * In a real implementation, this would derive from environment variables
21
- * For client-side use, this provides basic tamper resistance
67
+ *
68
+ * Returns environment-specific secrets for HMAC signing. In a real implementation,
69
+ * this would derive from environment variables. For client-side use, this provides
70
+ * basic tamper resistance only.
71
+ *
72
+ * WARNING: Client-side secrets are NOT secure against determined attackers.
73
+ * This is defense-in-depth only. Real security comes from server-side validation.
74
+ *
75
+ * @param {'production' | 'staging' | 'development'} environment - Target environment
76
+ * @returns {string} Environment-specific secret
77
+ * @export
22
78
  */
23
79
  export declare function getSigningSecret(environment: 'production' | 'staging' | 'development'): string;
24
80
  /**
25
81
  * Create signed state parameter with timestamp and integrity protection
82
+ *
83
+ * Creates a signed state parameter containing the payload with added timestamp
84
+ * and nonce for freshness and replay protection. The entire payload is signed
85
+ * using HMAC-SHA256 and base64 encoded for URL safety.
86
+ *
87
+ * @param {unknown} payload - Data to include in state parameter
88
+ * @param {'production' | 'staging' | 'development'} environment - Target environment
89
+ * @returns {Promise<string>} Promise resolving to base64-encoded signed state
90
+ * @export
26
91
  */
27
- export declare function createSignedState(payload: any, environment: 'production' | 'staging' | 'development'): Promise<string>;
92
+ export declare function createSignedState(payload: unknown, environment: 'production' | 'staging' | 'development'): Promise<string>;
28
93
  /**
29
94
  * Verify and parse signed state parameter
95
+ *
96
+ * Parses and verifies a signed state parameter, checking the HMAC signature
97
+ * and timestamp freshness. Returns the original payload if validation succeeds.
98
+ *
99
+ * @param {string} signedState - Base64-encoded signed state parameter
100
+ * @param {'production' | 'staging' | 'development'} environment - Source environment
101
+ * @param {number} [maxAge] - Maximum age in milliseconds
102
+ * @returns {Promise<unknown | null>} Promise resolving to payload or null if invalid
103
+ * @export
30
104
  */
31
- export declare function parseSignedState(signedState: string, environment: 'production' | 'staging' | 'development', maxAge?: number): Promise<any | null>;
105
+ export declare function parseSignedState(signedState: string, environment: 'production' | 'staging' | 'development', maxAge?: number): Promise<unknown | null>;
@@ -1,11 +1,42 @@
1
1
  /**
2
2
  * Crypto utilities for SafePassage SDK - Browser compatible
3
+ *
4
+ * Provides cryptographic functions for the SafePassage SDK using Web Crypto API.
5
+ * These utilities handle HMAC signing, state parameter protection, and secure
6
+ * token generation for client-side security measures.
7
+ *
8
+ * IMPORTANT SECURITY NOTE:
9
+ * Client-side cryptography provides defense-in-depth but cannot be considered
10
+ * secure against determined attackers. True security comes from server-side
11
+ * validation. These functions are primarily for tamper resistance and
12
+ * integrity checking.
13
+ *
14
+ * Features:
15
+ * - HMAC-SHA256 signing and verification using Web Crypto API
16
+ * - Constant-time string comparison to prevent timing attacks
17
+ * - Secure random token generation
18
+ * - Signed state parameter creation and parsing
19
+ * - Environment-specific secret derivation
20
+ * - Timestamp and nonce-based replay protection
21
+ *
22
+ * @author SafePassage Engineering
23
+ * @version 1.0.0
3
24
  */
4
25
  import { STATE_EXPIRY_MS } from './validation';
5
26
  /**
6
27
  * Generate HMAC-SHA256 signature using Web Crypto API
7
- * Note: This is client-side HMAC, the secret is known to the client.
8
- * For true security, verification should still happen server-side.
28
+ *
29
+ * Creates an HMAC-SHA256 signature for the given data using the provided secret.
30
+ * Uses the browser's Web Crypto API for cryptographic operations.
31
+ *
32
+ * SECURITY NOTE: This is client-side HMAC where the secret is known to the client.
33
+ * It provides tamper resistance but not true security. Server-side verification
34
+ * is required for actual security.
35
+ *
36
+ * @param {string} data - Data to sign
37
+ * @param {string} secret - HMAC secret key
38
+ * @returns {Promise<string>} Promise resolving to hex-encoded HMAC signature
39
+ * @export
9
40
  */
10
41
  export async function generateHMAC(data, secret) {
11
42
  // Encode the secret and data as Uint8Array
@@ -18,11 +49,20 @@ export async function generateHMAC(data, secret) {
18
49
  const signature = await crypto.subtle.sign('HMAC', key, dataBuffer);
19
50
  // Convert to hex string
20
51
  return Array.from(new Uint8Array(signature))
21
- .map(b => b.toString(16).padStart(2, '0'))
52
+ .map((b) => b.toString(16).padStart(2, '0'))
22
53
  .join('');
23
54
  }
24
55
  /**
25
56
  * Verify HMAC-SHA256 signature using Web Crypto API
57
+ *
58
+ * Verifies an HMAC-SHA256 signature against the provided data and secret.
59
+ * Uses constant-time comparison to prevent timing attacks.
60
+ *
61
+ * @param {string} data - Original data that was signed
62
+ * @param {string} signature - HMAC signature to verify
63
+ * @param {string} secret - HMAC secret key
64
+ * @returns {Promise<boolean>} Promise resolving to true if signature is valid
65
+ * @export
26
66
  */
27
67
  export async function verifyHMAC(data, signature, secret) {
28
68
  try {
@@ -35,6 +75,13 @@ export async function verifyHMAC(data, signature, secret) {
35
75
  }
36
76
  /**
37
77
  * Constant-time string comparison to prevent timing attacks
78
+ *
79
+ * Compares two strings in constant time to prevent timing-based attacks
80
+ * that could leak information about the expected signature.
81
+ *
82
+ * @param {string} a - First string to compare
83
+ * @param {string} b - Second string to compare
84
+ * @returns {boolean} True if strings are equal
38
85
  */
39
86
  function constantTimeCompare(a, b) {
40
87
  if (a.length !== b.length) {
@@ -48,36 +95,61 @@ function constantTimeCompare(a, b) {
48
95
  }
49
96
  /**
50
97
  * Generate a secure random token for client-side use
98
+ *
99
+ * Creates a cryptographically secure random token using the browser's
100
+ * crypto.getRandomValues() function.
101
+ *
102
+ * @param {number} [length=32] - Token length in bytes
103
+ * @returns {string} Hex-encoded random token
104
+ * @export
51
105
  */
52
106
  export function generateSecureToken(length = 32) {
53
107
  const array = new Uint8Array(length);
54
108
  crypto.getRandomValues(array);
55
- return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
109
+ return Array.from(array, (b) => b.toString(16).padStart(2, '0')).join('');
56
110
  }
57
111
  /**
58
112
  * Environment-specific secret derivation
59
- * In a real implementation, this would derive from environment variables
60
- * For client-side use, this provides basic tamper resistance
113
+ *
114
+ * Returns environment-specific secrets for HMAC signing. In a real implementation,
115
+ * this would derive from environment variables. For client-side use, this provides
116
+ * basic tamper resistance only.
117
+ *
118
+ * WARNING: Client-side secrets are NOT secure against determined attackers.
119
+ * This is defense-in-depth only. Real security comes from server-side validation.
120
+ *
121
+ * @param {'production' | 'staging' | 'development'} environment - Target environment
122
+ * @returns {string} Environment-specific secret
123
+ * @export
61
124
  */
62
125
  export function getSigningSecret(environment) {
63
126
  // WARNING: Client-side secrets are NOT secure against determined attackers
64
127
  // This is defense-in-depth only. Real security comes from server-side validation.
65
128
  const baseSecrets = {
66
129
  production: 'safepassage-prod-hmac-2025',
67
- staging: 'safepassage-stage-hmac-2025',
68
- development: 'safepassage-dev-hmac-2025'
130
+ staging: 'safepassage-prod-hmac-2025', // Use production secrets for staging
131
+ development: 'safepassage-dev-hmac-2025',
69
132
  };
70
133
  return baseSecrets[environment];
71
134
  }
72
135
  /**
73
136
  * Create signed state parameter with timestamp and integrity protection
137
+ *
138
+ * Creates a signed state parameter containing the payload with added timestamp
139
+ * and nonce for freshness and replay protection. The entire payload is signed
140
+ * using HMAC-SHA256 and base64 encoded for URL safety.
141
+ *
142
+ * @param {unknown} payload - Data to include in state parameter
143
+ * @param {'production' | 'staging' | 'development'} environment - Target environment
144
+ * @returns {Promise<string>} Promise resolving to base64-encoded signed state
145
+ * @export
74
146
  */
75
147
  export async function createSignedState(payload, environment) {
76
148
  // Add timestamp for freshness
77
149
  const timestampedPayload = {
78
150
  ...payload,
79
151
  timestamp: Date.now(),
80
- nonce: generateSecureToken(16) // Add nonce to prevent replay attacks
152
+ nonce: generateSecureToken(16), // Add nonce to prevent replay attacks
81
153
  };
82
154
  const dataString = JSON.stringify(timestampedPayload);
83
155
  const secret = getSigningSecret(environment);
@@ -85,12 +157,21 @@ export async function createSignedState(payload, environment) {
85
157
  // Combine data and signature
86
158
  const signedPayload = {
87
159
  data: timestampedPayload,
88
- signature
160
+ signature,
89
161
  };
90
162
  return btoa(JSON.stringify(signedPayload));
91
163
  }
92
164
  /**
93
165
  * Verify and parse signed state parameter
166
+ *
167
+ * Parses and verifies a signed state parameter, checking the HMAC signature
168
+ * and timestamp freshness. Returns the original payload if validation succeeds.
169
+ *
170
+ * @param {string} signedState - Base64-encoded signed state parameter
171
+ * @param {'production' | 'staging' | 'development'} environment - Source environment
172
+ * @param {number} [maxAge] - Maximum age in milliseconds
173
+ * @returns {Promise<unknown | null>} Promise resolving to payload or null if invalid
174
+ * @export
94
175
  */
95
176
  export async function parseSignedState(signedState, environment, maxAge = STATE_EXPIRY_MS) {
96
177
  try {
@@ -118,6 +199,7 @@ export async function parseSignedState(signedState, environment, maxAge = STATE_
118
199
  }
119
200
  }
120
201
  // Remove internal fields before returning
202
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
121
203
  const { timestamp, nonce, ...payload } = data;
122
204
  return payload;
123
205
  }
@@ -2,14 +2,15 @@
2
2
  * Environment utilities for SafePassage SDK with security enforcement
3
3
  */
4
4
  const ENVIRONMENT_URLS = {
5
- production: 'https://verify.safepassageapp.com',
6
- staging: 'https://verify-staging.safepassageapp.com',
7
- development: 'http://localhost:5173'
5
+ production: 'https://av.safepassageapp.com',
6
+ staging: 'https://av.safepassageapp.com',
7
+ development: 'http://localhost:5173',
8
8
  };
9
9
  export function getEnvironmentUrl(environment) {
10
10
  const url = ENVIRONMENT_URLS[environment];
11
11
  // Security check: Ensure production/staging always use HTTPS
12
- if ((environment === 'production' || environment === 'staging') && !url.startsWith('https://')) {
12
+ if ((environment === 'production' || environment === 'staging') &&
13
+ !url.startsWith('https://')) {
13
14
  throw new Error(`HTTPS required for ${environment} environment`);
14
15
  }
15
16
  return url;
@@ -17,12 +18,13 @@ export function getEnvironmentUrl(environment) {
17
18
  export function getApiUrl(environment) {
18
19
  const apiUrls = {
19
20
  production: 'https://api.safepassageapp.com',
20
- staging: 'https://api-staging.safepassageapp.com',
21
- development: 'http://localhost:3001'
21
+ staging: 'https://api.safepassageapp.com',
22
+ development: 'http://localhost:3001',
22
23
  };
23
24
  const url = apiUrls[environment];
24
25
  // Security check: Ensure production/staging always use HTTPS
25
- if ((environment === 'production' || environment === 'staging') && !url.startsWith('https://')) {
26
+ if ((environment === 'production' || environment === 'staging') &&
27
+ !url.startsWith('https://')) {
26
28
  throw new Error(`HTTPS required for API URLs in ${environment} environment`);
27
29
  }
28
30
  return url;
@@ -32,9 +34,12 @@ export function getApiUrl(environment) {
32
34
  */
33
35
  export function detectEnvironment() {
34
36
  const hostname = window.location.hostname;
35
- if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.includes('.local')) {
37
+ if (hostname === 'localhost' ||
38
+ hostname === '127.0.0.1' ||
39
+ hostname.includes('.local')) {
36
40
  // Warn if using HTTP in development with non-localhost domains
37
- if (window.location.protocol === 'http:' && !hostname.match(/^(localhost|127\.0\.0\.1)$/)) {
41
+ if (window.location.protocol === 'http:' &&
42
+ !hostname.match(/^(localhost|127\.0\.0\.1)$/)) {
38
43
  console.warn('SafePassage Security Warning: Using HTTP with non-localhost domain in development');
39
44
  }
40
45
  return 'development';
@@ -70,12 +75,15 @@ export function validateEnvironmentSecurity(environment) {
70
75
  console.warn('SafePassage Warning: HTTPS strongly recommended in staging environment');
71
76
  }
72
77
  break;
73
- case 'development':
74
- const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname.includes('.local');
78
+ case 'development': {
79
+ const isLocalhost = hostname === 'localhost' ||
80
+ hostname === '127.0.0.1' ||
81
+ hostname.includes('.local');
75
82
  if (!isSecure && !isLocalhost) {
76
83
  console.warn('SafePassage Warning: HTTPS recommended for non-localhost development');
77
84
  }
78
85
  break;
86
+ }
79
87
  }
80
88
  // Validate environment URLs
81
89
  try {
@@ -8,23 +8,26 @@
8
8
  export function setupPolyfills() {
9
9
  // Polyfill crypto.randomUUID if not available
10
10
  if (!crypto.randomUUID) {
11
- crypto.randomUUID = function () {
12
- // Generate 16 random bytes
13
- const array = new Uint8Array(16);
14
- crypto.getRandomValues(array);
15
- // Set version (4) and variant bits
16
- array[6] = (array[6] & 0x0f) | 0x40; // Version 4
17
- array[8] = (array[8] & 0x3f) | 0x80; // Variant 10
18
- // Convert to hex string with dashes
19
- const hex = Array.from(array).map(b => b.toString(16).padStart(2, '0')).join('');
20
- return [
21
- hex.slice(0, 8),
22
- hex.slice(8, 12),
23
- hex.slice(12, 16),
24
- hex.slice(16, 20),
25
- hex.slice(20, 32)
26
- ].join('-');
27
- };
11
+ crypto.randomUUID =
12
+ function () {
13
+ // Generate 16 random bytes
14
+ const array = new Uint8Array(16);
15
+ crypto.getRandomValues(array);
16
+ // Set version (4) and variant bits
17
+ array[6] = (array[6] & 0x0f) | 0x40; // Version 4
18
+ array[8] = (array[8] & 0x3f) | 0x80; // Variant 10
19
+ // Convert to hex string with dashes
20
+ const hex = Array.from(array)
21
+ .map((b) => b.toString(16).padStart(2, '0'))
22
+ .join('');
23
+ return [
24
+ hex.slice(0, 8),
25
+ hex.slice(8, 12),
26
+ hex.slice(12, 16),
27
+ hex.slice(16, 20),
28
+ hex.slice(20, 32),
29
+ ].join('-');
30
+ };
28
31
  }
29
32
  // Note: URLSearchParams is widely supported (IE 11 doesn't support it, but we're not targeting IE 11)
30
33
  // If needed, a polyfill can be added here
@@ -46,5 +46,5 @@ export declare const verificationRateLimit: VerificationRateLimit;
46
46
  /**
47
47
  * Security event logging for monitoring
48
48
  */
49
- export declare function logSecurityEvent(event: string, details: any): void;
49
+ export declare function logSecurityEvent(event: string, details: unknown): void;
50
50
  export {};