@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 +76 -31
- package/dist/index.d.ts +17 -2
- package/dist/safepassage.min.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,15 +22,20 @@ npm install @safepassage/sdk
|
|
|
22
22
|
|
|
23
23
|
```javascript
|
|
24
24
|
// Initialize SDK
|
|
25
|
-
const
|
|
26
|
-
apiKey: '
|
|
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
|
-
|
|
33
|
-
sessionId:
|
|
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
|
|
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
|
-
|
|
58
|
+
safePassage.verify({
|
|
54
59
|
sessionId: 'uuid-v4', // Required: merchant-generated UUID
|
|
55
|
-
challengeAge: 30, // Optional: min 25 (
|
|
56
|
-
verificationMode: 'L2' // Optional: 'L1' or 'L2' (
|
|
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
|
|
67
|
-
apiKey: '
|
|
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
|
-
|
|
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
|
|
80
|
-
apiKey: '
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
202
|
-
apiKey: '
|
|
246
|
+
const safePassage = new SafePassage({
|
|
247
|
+
apiKey: 'sk_live_xxxxx',
|
|
203
248
|
returnUrl: '/verified',
|
|
204
249
|
cancelUrl: '/cancelled'
|
|
205
250
|
});
|
|
206
|
-
|
|
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
|
|
55
|
+
* Required for private keys (sk_), optional for public keys (pk_)
|
|
56
|
+
* For public keys: SDK will generate session internally
|
|
56
57
|
*/
|
|
57
|
-
sessionId
|
|
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
|
+
}
|
package/dist/safepassage.min.js
CHANGED
|
@@ -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; }
|