@safepassage/sdk 3.0.0 → 3.0.1

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/README.md CHANGED
@@ -22,15 +22,20 @@ npm install @safepassage/sdk
22
22
 
23
23
  ```javascript
24
24
  // Initialize SDK
25
- const sp = new SafePassage({
26
- apiKey: 'pk_live_xxxxx',
25
+ const safePassage = new SafePassage({
26
+ apiKey: 'sk_live_xxxxx', // Your API key
27
27
  returnUrl: 'https://yoursite.com/verified',
28
28
  cancelUrl: 'https://yoursite.com/cancelled'
29
29
  });
30
30
 
31
+ // Generate a session ID (must be UUID v4)
32
+ const sessionId = crypto.randomUUID();
33
+
31
34
  // Trigger verification
32
- sp.verify({
33
- sessionId: generateUUID() // You must generate this
35
+ safePassage.verify({
36
+ sessionId: sessionId, // Required: merchant-generated UUID
37
+ challengeAge: 25, // Optional: minimum age (25 or higher)
38
+ verificationMode: 'L1' // Optional: 'L1' or 'L2'
34
39
  });
35
40
  ```
36
41
 
@@ -38,7 +43,7 @@ sp.verify({
38
43
 
39
44
  | Option | Type | Required | Description |
40
45
  |--------|------|----------|-------------|
41
- | apiKey | string | Yes | Your public API key (pk_live_xxx or pk_test_xxx) |
46
+ | apiKey | string | Yes | Your API key (sk_live_xxx or sk_test_xxx) |
42
47
  | returnUrl | string | Yes | URL to redirect after successful verification |
43
48
  | cancelUrl | string | Yes | URL to redirect if user cancels |
44
49
  | environment | string | No | 'production', 'staging', or 'development' (auto-detected) |
@@ -50,10 +55,46 @@ sp.verify({
50
55
  ## Verification Options
51
56
 
52
57
  ```javascript
53
- sp.verify({
58
+ safePassage.verify({
54
59
  sessionId: 'uuid-v4', // Required: merchant-generated UUID
55
- challengeAge: 30, // Optional: min 25 (default from dashboard)
56
- verificationMode: 'L2' // Optional: 'L1' or 'L2' (default from dashboard)
60
+ challengeAge: 30, // Optional: min 25 (overrides dashboard setting)
61
+ verificationMode: 'L2' // Optional: 'L1' or 'L2' (overrides dashboard setting)
62
+ });
63
+ ```
64
+
65
+ ### Configuration Override Behavior
66
+
67
+ When you pass `challengeAge` or `verificationMode` to the `verify()` method, these values take precedence over your dashboard configuration for that specific verification session:
68
+
69
+ - **No overrides**: Uses your current dashboard settings
70
+ - **With overrides**: SDK values are used instead of dashboard settings
71
+ - **Challenge age**: Must be 25 or higher (lower values will be rejected)
72
+ - **Verification mode**:
73
+ - `'L1'`: Age estimation with computer vision
74
+ - `'L2'`: Always requires ID verification
75
+
76
+ Example use cases:
77
+ ```javascript
78
+ // Use dashboard defaults
79
+ safePassage.verify({ sessionId: crypto.randomUUID() });
80
+
81
+ // Override just challenge age
82
+ safePassage.verify({
83
+ sessionId: crypto.randomUUID(),
84
+ challengeAge: 30 // Require age 30+ for this session
85
+ });
86
+
87
+ // Override just verification mode
88
+ safePassage.verify({
89
+ sessionId: crypto.randomUUID(),
90
+ verificationMode: 'L2' // Force ID check for this session
91
+ });
92
+
93
+ // Override both
94
+ safePassage.verify({
95
+ sessionId: crypto.randomUUID(),
96
+ challengeAge: 30,
97
+ verificationMode: 'L2' // ID required for 30+ verification
57
98
  });
58
99
  ```
59
100
 
@@ -63,21 +104,21 @@ sp.verify({
63
104
  User is redirected to SafePassage, then back to your site:
64
105
 
65
106
  ```javascript
66
- const sp = new SafePassage({
67
- apiKey: 'pk_live_xxxxx',
107
+ const safePassage = new SafePassage({
108
+ apiKey: 'sk_live_xxxxx',
68
109
  returnUrl: '/age-verified',
69
110
  cancelUrl: '/age-gate'
70
111
  });
71
112
 
72
- sp.verify({ sessionId: generateUUID() });
113
+ safePassage.verify({ sessionId: crypto.randomUUID() });
73
114
  ```
74
115
 
75
116
  ### New-Tab Mode
76
117
  Verification opens in a popup window:
77
118
 
78
119
  ```javascript
79
- const sp = new SafePassage({
80
- apiKey: 'pk_live_xxxxx',
120
+ const safePassage = new SafePassage({
121
+ apiKey: 'sk_live_xxxxx',
81
122
  returnUrl: '/age-verified',
82
123
  cancelUrl: '/age-gate',
83
124
  mode: 'new-tab',
@@ -90,7 +131,7 @@ const sp = new SafePassage({
90
131
  }
91
132
  });
92
133
 
93
- sp.verify({ sessionId: generateUUID() });
134
+ safePassage.verify({ sessionId: crypto.randomUUID() });
94
135
  ```
95
136
 
96
137
  ## Server-Side Validation (Required!)
@@ -116,9 +157,13 @@ if (result.verified && result.estimatedAge >= result.challengeAge) {
116
157
 
117
158
  ## UUID Generation
118
159
 
119
- You must generate session IDs on your end:
160
+ You must generate session IDs on your end. Use the built-in crypto.randomUUID() when available:
120
161
 
121
162
  ```javascript
163
+ // Modern browsers and Node.js 16+
164
+ const sessionId = crypto.randomUUID();
165
+
166
+ // Fallback for older environments
122
167
  function generateUUID() {
123
168
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
124
169
  const r = Math.random() * 16 | 0;
@@ -141,24 +186,24 @@ function generateUUID() {
141
186
  <button onclick="verifyAge()">Verify Your Age</button>
142
187
 
143
188
  <script>
144
- function generateUUID() {
145
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
146
- const r = Math.random() * 16 | 0;
147
- const v = c === 'x' ? r : (r & 0x3 | 0x8);
148
- return v.toString(16);
149
- });
150
- }
151
-
152
- const sp = new SafePassage({
153
- apiKey: 'pk_live_xxxxx',
189
+ const safePassage = new SafePassage({
190
+ apiKey: 'sk_live_xxxxx',
154
191
  returnUrl: window.location.href + '?verified=true',
155
192
  cancelUrl: window.location.href
156
193
  });
157
194
 
158
195
  function verifyAge() {
159
- const sessionId = generateUUID();
196
+ // Use crypto.randomUUID() if available, otherwise fallback
197
+ const sessionId = typeof crypto.randomUUID === 'function'
198
+ ? crypto.randomUUID()
199
+ : 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
200
+ const r = Math.random() * 16 | 0;
201
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
202
+ return v.toString(16);
203
+ });
204
+
160
205
  sessionStorage.setItem('pendingVerification', sessionId);
161
- sp.verify({ sessionId });
206
+ safePassage.verify({ sessionId });
162
207
  }
163
208
 
164
209
  // Check if returning from verification
@@ -186,7 +231,7 @@ const config: SafePassageConfig = {
186
231
  cancelUrl: '/cancelled'
187
232
  };
188
233
 
189
- const sp = new SafePassage(config);
234
+ const safePassage = new SafePassage(config);
190
235
  ```
191
236
 
192
237
  ## Migration from v2 (iframe)
@@ -198,12 +243,12 @@ Old iframe approach (1000+ lines):
198
243
 
199
244
  New redirect approach (10 lines):
200
245
  ```javascript
201
- const sp = new SafePassage({
202
- apiKey: 'pk_live_xxxxx',
246
+ const safePassage = new SafePassage({
247
+ apiKey: 'sk_live_xxxxx',
203
248
  returnUrl: '/verified',
204
249
  cancelUrl: '/cancelled'
205
250
  });
206
- sp.verify({ sessionId: generateUUID() });
251
+ safePassage.verify({ sessionId: crypto.randomUUID() });
207
252
  ```
208
253
 
209
254
  ## Browser Support
package/dist/index.d.ts CHANGED
@@ -52,9 +52,10 @@ export interface SafePassageConfig {
52
52
  export interface VerificationOptions {
53
53
  /**
54
54
  * Merchant-generated UUID v4 for this verification session
55
- * Required for security - prevents session fixation attacks
55
+ * Required for private keys (sk_), optional for public keys (pk_)
56
+ * For public keys: SDK will generate session internally
56
57
  */
57
- sessionId: string;
58
+ sessionId?: string;
58
59
  /**
59
60
  * Minimum age to verify (minimum 25)
60
61
  * @default Uses merchant dashboard configuration
@@ -86,6 +87,7 @@ export interface StatePayload {
86
87
  cancelUrl: string;
87
88
  challengeAge?: number;
88
89
  verificationMode?: 'L1' | 'L2';
90
+ hasOverrides?: boolean;
89
91
  timestamp: number;
90
92
  }
91
93
  export interface SessionValidationResponse {
@@ -100,3 +102,16 @@ export interface SessionValidationResponse {
100
102
  timestamp: string;
101
103
  expiresAt: string;
102
104
  }
105
+ export interface SessionCreationResponse {
106
+ sessionToken: string;
107
+ verifyUrl: string;
108
+ expiresAt: string;
109
+ }
110
+ export interface CreateSessionRequest {
111
+ sessionId: string;
112
+ returnUrl: string;
113
+ cancelUrl?: string;
114
+ challengeAge?: number;
115
+ verificationMode?: 'L1' | 'L2';
116
+ merchantName?: string;
117
+ }
@@ -1,3 +1,3 @@
1
1
  /* SafePassage SDK v3.0.0 - Redirect Implementation */
2
- "use strict";var SafePassageSDK=(()=>{var p=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var K=Object.prototype.hasOwnProperty;var c=(t,e)=>()=>(t&&(e=t(t=0)),e);var u=(t,e)=>{for(var n in e)p(t,n,{get:e[n],enumerable:!0})},q=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of H(e))!K.call(t,o)&&o!==n&&p(t,o,{get:()=>e[o],enumerable:!(i=W(e,o))||i.enumerable});return t};var I=t=>q(p({},"__esModule",{value:!0}),t);function j(t,e){return E[e].includes(t)}function P(t,e,n=[]){let{origin:i}=t;return j(i,e)||n.length>0&&n.some(r=>{if(r.startsWith("*.")){let a=r.slice(2);return i.endsWith(`.${a}`)||i===`https://${a}`||i===`http://${a}`}return i===r})?!0:(console.warn(`SafePassage Security: Blocked PostMessage from untrusted origin: ${i}`,{environment:e,trustedOrigins:E[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 U(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 f(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 o of i)if(o.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 E,g,V,h=c(()=>{"use strict";E={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"]};g=class{constructor(){this.attempts=new Map;this.maxAttempts=5;this.timeWindow=6e4}isAllowed(e){let n=Date.now(),o=(this.attempts.get(e)||[]).filter(r=>n-r<this.timeWindow);return o.length>=this.maxAttempts?(console.warn(`SafePassage Security: Rate limit exceeded for ${e}`),!1):(o.push(n),this.attempts.set(e,o),!0)}reset(e){this.attempts.delete(e)}},V=new g});var L={};u(L,{createSignedState:()=>J,generateHMAC:()=>m,generateSecureToken:()=>b,getSigningSecret:()=>w,parseSignedState:()=>B,verifyHMAC:()=>A});async function m(t,e){let n=new TextEncoder,i=n.encode(e),o=n.encode(t),r=await crypto.subtle.importKey("raw",i,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),a=await crypto.subtle.sign("HMAC",r,o);return Array.from(new Uint8Array(a)).map(d=>d.toString(16).padStart(2,"0")).join("")}async function A(t,e,n){try{let i=await m(t,n);return F(e,i)}catch{return!1}}function F(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 b(t=32){let e=new Uint8Array(t);return crypto.getRandomValues(e),Array.from(e,n=>n.toString(16).padStart(2,"0")).join("")}function w(t){return{production:"safepassage-prod-hmac-2025",staging:"safepassage-stage-hmac-2025",development:"safepassage-dev-hmac-2025"}[t]}async function J(t,e){let n={...t,timestamp:Date.now(),nonce:b(16)},i=JSON.stringify(n),o=w(e),r=await m(i,o);return btoa(JSON.stringify({data:n,signature:r}))}async function B(t,e,n=10*60*1e3){try{let i=atob(t),o=JSON.parse(i);if(!o.data||!o.signature)return console.warn("SafePassage: Invalid signed state format"),null;let{data:r,signature:a}=o,d=JSON.stringify(r),k=w(e);if(!await A(d,a,k))return console.warn("SafePassage: State signature verification failed"),null;if(r.timestamp){let y=Date.now()-r.timestamp;if(y>n)return console.warn("SafePassage: State parameter expired",{age:y,maxAge:n}),null}let{timestamp:ne,nonce:ie,...$}=r;return $}catch(i){return console.warn("SafePassage: Failed to parse signed state",i),null}}var x=c(()=>{"use strict"});function C(t){if(!t.apiKey)throw new Error("apiKey is required");if(!G.test(t.apiKey))throw new Error("Invalid apiKey format. Expected pk_xxx or sk_xxx");if(!t.returnUrl)throw new Error("returnUrl is required");if(!t.cancelUrl)throw new Error("cancelUrl is required");let e=Y(),n=f(t.returnUrl,e);if(!n.isValid)throw new Error(`returnUrl validation failed: ${n.error}`);let i=f(t.cancelUrl,e);if(!i.isValid)throw new Error(`cancelUrl validation failed: ${i.error}`);if(t.defaultChallengeAge!==void 0&&t.defaultChallengeAge<M)throw new Error(`defaultChallengeAge must be at least ${M}`);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 Y(){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 R(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(x(),L));return n(t,e)}var M,G,O=c(()=>{"use strict";h();M=25,G=/^(pk|sk)_[a-zA-Z0-9]+$/});function v(t){let e=Z[t];if((t==="production"||t==="staging")&&!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function z(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 D(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{v(t),z(t)}catch(i){throw new Error(`Environment configuration validation failed: ${i}`)}}var Z,N=c(()=>{"use strict";Z={production:"https://verify.safepassageapp.com",staging:"https://verify-staging.safepassageapp.com",development:"http://localhost:5173"}});var _={};u(_,{SafePassage:()=>l,default:()=>X});var l,X,S=c(()=>{"use strict";O();N();h();l=class{constructor(e){this.popupWindow=null;this.messageListener=null;this.popupMonitorInterval=null;this.unloadListener=null;this.isVerificationInProgress=!1;this.currentSessionId=null;C(e),this.config={...e,environment:e.environment||this.detectEnvironment(),mode:e.mode||"redirect"},D(this.config.environment),U(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){if(!e.sessionId)throw new Error("sessionId is required - must be a merchant-generated UUID v4");if(this.isVerificationInProgress){let n=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.substring(0,8)+"...",origin:window.location.origin}),this.config.onError?.(n),n}this.isVerificationInProgress=!0,this.currentSessionId=e.sessionId;try{let n=`${this.config.apiKey}:${window.location.origin}`;if(!V.isAllowed(n)){let o=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:e.sessionId.substring(0,8)+"..."}),this.config.onError?.(o),o}let i=await this.buildVerificationUrl(e);s("VERIFICATION_INITIATED",{environment:this.config.environment,mode:this.config.mode,sessionId:e.sessionId.substring(0,8)+"...",origin:window.location.origin}),this.config.mode==="new-tab"?this.openNewTab(i,e.sessionId):(this.unlockVerification(),this.redirect(i))}catch(n){throw this.unlockVerification(),n}}async buildVerificationUrl(e){let n=v(this.config.environment),i=await R({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,timestamp:Date.now()},this.config.environment),o=new URLSearchParams({state:i,sessionId:e.sessionId,mode:this.config.mode});return`${n}/verify?${o.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(!P(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 o=T(i,n);if(!o.isValid){s("POSTMESSAGE_VALIDATION_FAILED",{error:o.error,origin:i.origin,sessionId:n.substring(0,8)+"...",messageType:i.data?.type});return}let r={sessionId:i.data.sessionId,status:i.data.status};s("VERIFICATION_COMPLETED",{status:r.status,sessionId:n.substring(0,8)+"...",origin:i.origin}),this.cleanup(),this.unlockVerification(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),r.status==="verified"?this.config.onComplete?.(r):r.status==="cancelled"?this.config.onCancel?.():this.config.onError?.(new Error(`Verification failed: ${r.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()}},X=l});var ee={};u(ee,{SafePassage:()=>l,VERSION:()=>Q,default:()=>l});S();var Q="3.0.0";typeof window<"u"&&window&&(window.SafePassage=(S(),I(_)).SafePassage);return I(ee);})();
2
+ "use strict";var SafePassageSDK=(()=>{var d=Object.defineProperty;var K=Object.getOwnPropertyDescriptor;var W=Object.getOwnPropertyNames;var H=Object.prototype.hasOwnProperty;var p=(t,e)=>()=>(t&&(e=t(t=0)),e);var u=(t,e)=>{for(var i in e)d(t,i,{get:e[i],enumerable:!0})},q=(t,e,i,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of W(e))!H.call(t,o)&&o!==i&&d(t,o,{get:()=>e[o],enumerable:!(n=K(e,o))||n.enumerable});return t};var I=t=>q(d({},"__esModule",{value:!0}),t);function F(t,e){return E[e].includes(t)}function P(t,e,i=[]){let{origin:n}=t;return F(n,e)||i.length>0&&i.some(r=>{if(r.startsWith("*.")){let s=r.slice(2);return n.endsWith(`.${s}`)||n===`https://${s}`||n===`http://${s}`}return n===r})?!0:(console.warn(`SafePassage Security: Blocked PostMessage from untrusted origin: ${n}`,{environment:e,trustedOrigins:E[e],allowedCustomOrigins:i,eventType:t.data?.type}),!1)}function T(t,e){let{data:i}=t;return!i||typeof i!="object"?{isValid:!1,error:"Invalid message format"}:i.type!=="safepassage:verification:complete"?{isValid:!1,error:"Invalid message type"}:!i.sessionId||i.sessionId!==e?{isValid:!1,error:"Session ID mismatch"}:!i.status||!["verified","failed","cancelled"].includes(i.status)?{isValid:!1,error:"Invalid status value"}:{isValid:!0}}function U(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 f(t,e){try{let i=new URL(t);if(e==="production"&&i.protocol!=="https:")return{isValid:!1,error:"HTTPS required for return URLs in production"};if(e==="development"&&!(i.hostname==="localhost"||i.hostname==="127.0.0.1"||i.hostname.endsWith(".local"))&&i.protocol!=="https:")return{isValid:!1,error:"Non-localhost URLs must use HTTPS"};if(e==="staging"&&i.protocol!=="https:")return{isValid:!1,error:"HTTPS required for return URLs in staging"};let n=[/data:/i,/javascript:/i,/vbscript:/i,/file:/i,/ftp:/i];for(let o of n)if(o.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 E,g,A,h=p(()=>{"use strict";E={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"]};g=class{constructor(){this.attempts=new Map;this.maxAttempts=5;this.timeWindow=6e4}isAllowed(e){let i=Date.now(),o=(this.attempts.get(e)||[]).filter(r=>i-r<this.timeWindow);return o.length>=this.maxAttempts?(console.warn(`SafePassage Security: Rate limit exceeded for ${e}`),!1):(o.push(i),this.attempts.set(e,o),!0)}reset(e){this.attempts.delete(e)}},A=new g});var L={};u(L,{createSignedState:()=>J,generateHMAC:()=>m,generateSecureToken:()=>V,getSigningSecret:()=>w,parseSignedState:()=>B,verifyHMAC:()=>b});async function m(t,e){let i=new TextEncoder,n=i.encode(e),o=i.encode(t),r=await crypto.subtle.importKey("raw",n,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),s=await crypto.subtle.sign("HMAC",r,o);return Array.from(new Uint8Array(s)).map(l=>l.toString(16).padStart(2,"0")).join("")}async function b(t,e,i){try{let n=await m(t,i);return j(e,n)}catch{return!1}}function j(t,e){if(t.length!==e.length)return!1;let i=0;for(let n=0;n<t.length;n++)i|=t.charCodeAt(n)^e.charCodeAt(n);return i===0}function V(t=32){let e=new Uint8Array(t);return crypto.getRandomValues(e),Array.from(e,i=>i.toString(16).padStart(2,"0")).join("")}function w(t){return{production:"safepassage-prod-hmac-2025",staging:"safepassage-stage-hmac-2025",development:"safepassage-dev-hmac-2025"}[t]}async function J(t,e){let i={...t,timestamp:Date.now(),nonce:V(16)},n=JSON.stringify(i),o=w(e),r=await m(n,o);return btoa(JSON.stringify({data:i,signature:r}))}async function B(t,e,i=10*60*1e3){try{let n=atob(t),o=JSON.parse(n);if(!o.data||!o.signature)return console.warn("SafePassage: Invalid signed state format"),null;let{data:r,signature:s}=o,l=JSON.stringify(r),$=w(e);if(!await b(l,s,$))return console.warn("SafePassage: State signature verification failed"),null;if(r.timestamp){let y=Date.now()-r.timestamp;if(y>i)return console.warn("SafePassage: State parameter expired",{age:y,maxAge:i}),null}let{timestamp:ie,nonce:ne,...k}=r;return k}catch(n){return console.warn("SafePassage: Failed to parse signed state",n),null}}var x=p(()=>{"use strict"});function M(t){if(!t.apiKey)throw new Error("apiKey is required");if(!G.test(t.apiKey))throw new Error("Invalid apiKey format. Expected pk_xxx (public) or sk_xxx (private)");if(!t.returnUrl)throw new Error("returnUrl is required");if(!t.cancelUrl)throw new Error("cancelUrl is required");let e=Y(),i=f(t.returnUrl,e);if(!i.isValid)throw new Error(`returnUrl validation failed: ${i.error}`);let n=f(t.cancelUrl,e);if(!n.isValid)throw new Error(`cancelUrl validation failed: ${n.error}`);if(t.defaultChallengeAge!==void 0&&t.defaultChallengeAge<C)throw new Error(`defaultChallengeAge must be at least ${C}`);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 Y(){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 R(t,e){let{createSignedState:i}=await Promise.resolve().then(()=>(x(),L));return i(t,e)}var C,G,O=p(()=>{"use strict";h();C=25,G=/^(pk_|sk_)[a-zA-Z0-9]+$/});function v(t){let e=z[t];if((t==="production"||t==="staging")&&!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function Z(t){let i={production:"https://api.safepassageapp.com",staging:"https://api-staging.safepassageapp.com",development:"http://localhost:3001"}[t];if((t==="production"||t==="staging")&&!i.startsWith("https://"))throw new Error(`HTTPS required for API URLs in ${t} environment`);return i}function D(t){let e=window.location.protocol==="https:",i=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 n=i==="localhost"||i==="127.0.0.1"||i.includes(".local");!e&&!n&&console.warn("SafePassage Warning: HTTPS recommended for non-localhost development");break}try{v(t),Z(t)}catch(n){throw new Error(`Environment configuration validation failed: ${n}`)}}var z,N=p(()=>{"use strict";z={production:"https://verify.safepassageapp.com",staging:"https://verify-staging.safepassageapp.com",development:"http://localhost:5173"}});var _={};u(_,{SafePassage:()=>c,default:()=>X});var c,X,S=p(()=>{"use strict";O();N();h();c=class{constructor(e){this.popupWindow=null;this.messageListener=null;this.popupMonitorInterval=null;this.unloadListener=null;this.isVerificationInProgress=!1;this.currentSessionId=null;M(e),this.config={...e,environment:e.environment||this.detectEnvironment(),mode:e.mode||"redirect"},D(this.config.environment),U(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 i=this.isPublicKey(),n=e.sessionId;if(i&&!n&&(n=await this.createInternalSession(e)),!i&&!n)throw new Error("sessionId is required for private API keys - must be a merchant-generated UUID v4");if(!n)throw new Error("Failed to create or obtain sessionId");if(this.isVerificationInProgress){let o=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?.(o),o}this.isVerificationInProgress=!0,this.currentSessionId=n;try{let o=`${this.config.apiKey}:${window.location.origin}`;if(!A.isAllowed(o)){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:n?n.substring(0,8)+"...":"undefined"}),this.config.onError?.(s),s}let r=await this.buildVerificationUrl({...e,sessionId:n});a("VERIFICATION_INITIATED",{environment:this.config.environment,mode:this.config.mode,sessionId:n?n.substring(0,8)+"...":"undefined",origin:window.location.origin}),this.config.mode==="new-tab"?this.openNewTab(r,n):(this.unlockVerification(),this.redirect(r))}catch(o){throw this.unlockVerification(),o}}async buildVerificationUrl(e){let i=v(this.config.environment),n=e.challengeAge!==void 0,o=e.verificationMode!==void 0,r=n||o,s=await R({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:r,timestamp:Date.now()},this.config.environment),l=new URLSearchParams({state:s,sessionId:e.sessionId,mode:this.config.mode});return`${i}/verify?${l.toString()}`}redirect(e){window.location.href=e}openNewTab(e,i){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=n=>{if(!P(n,this.config.environment)){a("POSTMESSAGE_ORIGIN_BLOCKED",{origin:n.origin,environment:this.config.environment,expectedOrigins:`SafePassage trusted origins for ${this.config.environment}`,messageType:n.data?.type});return}let o=T(n,i);if(!o.isValid){a("POSTMESSAGE_VALIDATION_FAILED",{error:o.error,origin:n.origin,sessionId:i.substring(0,8)+"...",messageType:n.data?.type});return}let r={sessionId:n.data.sessionId,status:n.data.status};a("VERIFICATION_COMPLETED",{status:r.status,sessionId:i.substring(0,8)+"...",origin:n.origin}),this.cleanup(),this.unlockVerification(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),r.status==="verified"?this.config.onComplete?.(r):r.status==="cancelled"?this.config.onCancel?.():this.config.onError?.(new Error(`Verification failed: ${r.status}`))},window.addEventListener("message",this.messageListener),this.popupMonitorInterval=setInterval(()=>{this.popupWindow&&this.popupWindow.closed&&(a("POPUP_CLOSED_BY_USER",{sessionId:i.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=(...i)=>(this.cleanup(),this.unlockVerification(),e.apply(window.history,i))}}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 i=crypto.randomUUID();try{let n=this.getPortalApiUrl(),o=await fetch(`${n}/api/v1/sessions/create`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({sessionId:i,returnUrl:this.config.returnUrl,cancelUrl:this.config.cancelUrl,challengeAge:e.challengeAge,verificationMode:e.verificationMode,merchantName:document.title||window.location.hostname})});if(!o.ok){let s=await o.json().catch(()=>({}));throw new Error(`Failed to create session: ${o.status} ${o.statusText}. ${s.message||""}`)}let r=await o.json();return a("INTERNAL_SESSION_CREATED",{sessionId:i.substring(0,8)+"...",environment:this.config.environment,apiKeyType:"public"}),i}catch(n){let o=n instanceof Error?n.message:String(n);throw a("INTERNAL_SESSION_FAILED",{error:o,environment:this.config.environment,apiKeyType:"public"}),this.config.onError?.(n),new Error(`Failed to create verification session: ${o}`)}}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"}}},X=c});var ee={};u(ee,{SafePassage:()=>c,VERSION:()=>Q,default:()=>c});S();var Q="3.0.0";typeof window<"u"&&window&&(window.SafePassage=(S(),I(_)).SafePassage);return I(ee);})();
3
3
  if(typeof SafePassageSDK !== "undefined" && SafePassageSDK.SafePassage) { window.SafePassage = SafePassageSDK.SafePassage; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safepassage/sdk",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "SafePassage SDK - Lightweight redirect-based age verification",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",