@safepassage/sdk 3.0.14 → 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.
- package/dist/core/SafePassageSDK.d.ts +4 -3
- package/dist/core/SafePassageSDK.js +3 -13
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/safepassage.min.js +2 -2
- package/dist/types/index.d.ts +3 -3
- package/dist/utils/crypto.d.ts +6 -6
- package/dist/utils/crypto.js +3 -4
- package/dist/utils/environment.d.ts +5 -5
- package/dist/utils/environment.js +8 -32
- package/dist/utils/security.d.ts +4 -4
- package/dist/utils/security.js +7 -36
- package/dist/utils/validation.d.ts +2 -2
- package/dist/utils/validation.js +1 -6
- package/package.json +1 -1
- package/dist/tests/SafePassageSDK.test.d.ts +0 -4
- package/dist/tests/SafePassageSDK.test.js +0 -130
|
@@ -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'
|
|
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
|
|
137
|
+
* @returns {string} The current environment (production or staging)
|
|
137
138
|
*/
|
|
138
|
-
getEnvironment(): 'production' | 'staging'
|
|
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'
|
|
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
|
|
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':
|
package/dist/index.d.ts
CHANGED
|
@@ -18,4 +18,4 @@
|
|
|
18
18
|
*/
|
|
19
19
|
export { SafePassage, SafePassage as default } from './core/SafePassageSDK';
|
|
20
20
|
export type { SafePassageConfig, VerificationOptions, VerificationResult, SessionValidationResponse, } from './types';
|
|
21
|
-
export declare const VERSION = "3.0.
|
|
21
|
+
export declare const VERSION = "3.0.15";
|
package/dist/index.js
CHANGED
|
@@ -24,8 +24,8 @@ if (typeof window !== 'undefined') {
|
|
|
24
24
|
checkBrowserCompatibility();
|
|
25
25
|
}
|
|
26
26
|
export { SafePassage, SafePassage as default } from './core/SafePassageSDK';
|
|
27
|
-
// Version - Updated
|
|
28
|
-
export const VERSION = '3.0.
|
|
27
|
+
// Version - Updated for handoffToken QR code fix
|
|
28
|
+
export const VERSION = '3.0.15';
|
|
29
29
|
// For UMD builds
|
|
30
30
|
if (typeof window !== 'undefined' && window) {
|
|
31
31
|
// Dynamic import for UMD builds
|
package/dist/safepassage.min.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/* SafePassage SDK v3.0.
|
|
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.13";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; }
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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'
|
|
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'
|
|
106
|
+
environment?: 'production' | 'staging';
|
|
107
107
|
features?: {
|
|
108
108
|
testMode: boolean;
|
|
109
109
|
warmupPeriodMs: number;
|
package/dist/utils/crypto.d.ts
CHANGED
|
@@ -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'
|
|
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'
|
|
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'
|
|
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'
|
|
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'
|
|
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'
|
|
105
|
+
export declare function parseSignedState(signedState: string, environment: 'production' | 'staging', maxAge?: number): Promise<unknown | null>;
|
package/dist/utils/crypto.js
CHANGED
|
@@ -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'
|
|
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'
|
|
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'
|
|
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'
|
|
5
|
-
export declare function getApiUrl(environment: 'production' | 'staging'
|
|
4
|
+
export declare function getEnvironmentUrl(environment: 'production' | 'staging'): string;
|
|
5
|
+
export declare function getApiUrl(environment: 'production' | 'staging'): string;
|
|
6
6
|
/**
|
|
7
|
-
* Detect environment
|
|
7
|
+
* Detect environment - always defaults to production unless staging is detected
|
|
8
8
|
*/
|
|
9
|
-
export declare function detectEnvironment(): 'production' | 'staging'
|
|
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'
|
|
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 ((
|
|
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
|
|
22
|
-
development: 'http://localhost:3001',
|
|
19
|
+
staging: 'https://api.staging.safepassageapp.com',
|
|
23
20
|
};
|
|
24
21
|
const url = apiUrls[environment];
|
|
25
|
-
// Security check: Ensure
|
|
26
|
-
if ((
|
|
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
|
|
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
|
-
//
|
|
40
|
+
// Default to production environment - strict HTTPS enforcement
|
|
55
41
|
if (window.location.protocol !== 'https:') {
|
|
56
|
-
console.
|
|
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
|
-
|
|
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 {
|
package/dist/utils/security.d.ts
CHANGED
|
@@ -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'
|
|
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'
|
|
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'
|
|
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'
|
|
27
|
+
export declare function validateReturnUrl(url: string, environment: 'production' | 'staging'): {
|
|
28
28
|
isValid: boolean;
|
|
29
29
|
error?: string;
|
|
30
30
|
};
|
package/dist/utils/security.js
CHANGED
|
@@ -13,19 +13,9 @@ const TRUSTED_ORIGINS = {
|
|
|
13
13
|
'https://api.safepassageapp.com',
|
|
14
14
|
],
|
|
15
15
|
staging: [
|
|
16
|
-
'https://av
|
|
17
|
-
'https://portal
|
|
18
|
-
'https://api
|
|
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
|
-
|
|
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
|
|
116
|
-
if (
|
|
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:
|
|
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'
|
|
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'
|
|
21
|
+
export declare function parseState(state: string, environment: 'production' | 'staging'): Promise<StatePayload | null>;
|
package/dist/utils/validation.js
CHANGED
|
@@ -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,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
|
-
});
|