@safepassage/sdk 3.2.4 → 3.4.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/README.md +44 -17
- package/dist/core/SafePassageSDK.js +34 -18
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/safepassage.min.js +1 -1
- package/dist/types/index.d.ts +17 -3
- package/dist/utils/crypto.js +14 -7
- package/dist/utils/polyfills.js +0 -11
- package/dist/utils/security.js +4 -8
- package/dist/utils/validation.js +13 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,8 +31,7 @@ npm install @safepassage/sdk
|
|
|
31
31
|
// Initialize SDK
|
|
32
32
|
const safePassage = new SafePassage({
|
|
33
33
|
apiKey: 'sk_xxxxx', // Your API key
|
|
34
|
-
returnUrl: 'https://yoursite.com/verified'
|
|
35
|
-
cancelUrl: 'https://yoursite.com/cancelled'
|
|
34
|
+
returnUrl: 'https://yoursite.com/verified'
|
|
36
35
|
});
|
|
37
36
|
|
|
38
37
|
// Generate a session ID (must be UUID v4)
|
|
@@ -52,11 +51,9 @@ safePassage.verify({
|
|
|
52
51
|
|--------|------|----------|-------------|
|
|
53
52
|
| apiKey | string | Yes | Your API key (pk_xxx for client-side, sk_xxx for server-side) |
|
|
54
53
|
| returnUrl | string | Yes | URL to redirect after successful verification |
|
|
55
|
-
| cancelUrl | string | Yes | URL to redirect if user cancels |
|
|
56
54
|
| environment | string | No | 'production', 'staging', or 'development' (auto-detected) |
|
|
57
55
|
| mode | string | No | 'redirect' (default) or 'new-tab' |
|
|
58
56
|
| onComplete | function | No | Callback for new-tab mode completion |
|
|
59
|
-
| onCancel | function | No | Callback for new-tab mode cancellation |
|
|
60
57
|
| onError | function | No | Error handler |
|
|
61
58
|
|
|
62
59
|
### API Key Types
|
|
@@ -127,8 +124,7 @@ User is redirected to SafePassage, then back to your site:
|
|
|
127
124
|
```javascript
|
|
128
125
|
const safePassage = new SafePassage({
|
|
129
126
|
apiKey: 'sk_xxxxx',
|
|
130
|
-
returnUrl: '/age-verified'
|
|
131
|
-
cancelUrl: '/age-gate'
|
|
127
|
+
returnUrl: '/age-verified'
|
|
132
128
|
});
|
|
133
129
|
|
|
134
130
|
safePassage.verify({ sessionId: crypto.randomUUID() });
|
|
@@ -141,20 +137,54 @@ Verification opens in a popup window:
|
|
|
141
137
|
const safePassage = new SafePassage({
|
|
142
138
|
apiKey: 'sk_xxxxx',
|
|
143
139
|
returnUrl: '/age-verified',
|
|
144
|
-
cancelUrl: '/age-gate',
|
|
145
140
|
mode: 'new-tab',
|
|
146
141
|
onComplete: (result) => {
|
|
147
142
|
console.log('Verified:', result.sessionId);
|
|
148
143
|
// Validate on your server!
|
|
149
|
-
},
|
|
150
|
-
onCancel: () => {
|
|
151
|
-
console.log('User cancelled');
|
|
152
144
|
}
|
|
153
145
|
});
|
|
154
146
|
|
|
155
147
|
safePassage.verify({ sessionId: crypto.randomUUID() });
|
|
156
148
|
```
|
|
157
149
|
|
|
150
|
+
## URL Query Parameters
|
|
151
|
+
|
|
152
|
+
SafePassage supports optional query parameters for streamlined verification flows:
|
|
153
|
+
|
|
154
|
+
### skip_intro
|
|
155
|
+
Skip the introductory screen and navigate directly to camera access:
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
// Via SDK (automatic)
|
|
159
|
+
safePassage.verify({ sessionId: crypto.randomUUID() });
|
|
160
|
+
|
|
161
|
+
// Via server-side session creation
|
|
162
|
+
const url = new URL(verifyUrl);
|
|
163
|
+
url.searchParams.set('skip_intro', 'true');
|
|
164
|
+
window.location.href = url.toString();
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### auto_return
|
|
168
|
+
Automatically redirect to `returnUrl` after successful verification (redirect mode only):
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
// Via server-side session creation
|
|
172
|
+
const url = new URL(verifyUrl);
|
|
173
|
+
url.searchParams.set('auto_return', 'true');
|
|
174
|
+
window.location.href = url.toString();
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Combined Example
|
|
178
|
+
```javascript
|
|
179
|
+
const url = new URL(verifyUrl);
|
|
180
|
+
url.searchParams.set('skip_intro', 'true');
|
|
181
|
+
url.searchParams.set('auto_return', 'true');
|
|
182
|
+
// Result: Streamlined flow with minimal user interaction
|
|
183
|
+
window.location.href = url.toString();
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**For comprehensive parameter documentation, usage examples, and integration patterns, see [URL Parameters Reference](../../../docs/technical/api/VERIFY_UI_PARAMETERS.md).**
|
|
187
|
+
|
|
158
188
|
## Server-Side Validation (Required!)
|
|
159
189
|
|
|
160
190
|
Always validate the session on your server:
|
|
@@ -209,8 +239,7 @@ function generateUUID() {
|
|
|
209
239
|
<script>
|
|
210
240
|
const safePassage = new SafePassage({
|
|
211
241
|
apiKey: 'sk_xxxxx',
|
|
212
|
-
returnUrl: window.location.href + '?verified=true'
|
|
213
|
-
cancelUrl: window.location.href
|
|
242
|
+
returnUrl: window.location.href + '?verified=true'
|
|
214
243
|
});
|
|
215
244
|
|
|
216
245
|
function verifyAge() {
|
|
@@ -248,8 +277,7 @@ import { SafePassage, SafePassageConfig } from '@safepassage/sdk';
|
|
|
248
277
|
|
|
249
278
|
const config: SafePassageConfig = {
|
|
250
279
|
apiKey: process.env.SAFEPASSAGE_PUBLIC_KEY!,
|
|
251
|
-
returnUrl: '/verified'
|
|
252
|
-
cancelUrl: '/cancelled'
|
|
280
|
+
returnUrl: '/verified'
|
|
253
281
|
};
|
|
254
282
|
|
|
255
283
|
const safePassage = new SafePassage(config);
|
|
@@ -261,8 +289,7 @@ New streamlined approach (10 lines):
|
|
|
261
289
|
```javascript
|
|
262
290
|
const safePassage = new SafePassage({
|
|
263
291
|
apiKey: 'sk_xxxxx',
|
|
264
|
-
returnUrl: '/verified'
|
|
265
|
-
cancelUrl: '/cancelled'
|
|
292
|
+
returnUrl: '/verified'
|
|
266
293
|
});
|
|
267
294
|
safePassage.verify({ sessionId: crypto.randomUUID() });
|
|
268
295
|
```
|
|
@@ -280,4 +307,4 @@ safePassage.verify({ sessionId: crypto.randomUUID() });
|
|
|
280
307
|
1. Always generate session IDs on the merchant side
|
|
281
308
|
2. Validate sessions server-side before granting access
|
|
282
309
|
3. Pre-register callback URLs in your dashboard
|
|
283
|
-
4. Never expose your secret key (sk_xxx)
|
|
310
|
+
4. Never expose your secret key (sk_xxx)
|
|
@@ -70,11 +70,7 @@ export class SafePassage {
|
|
|
70
70
|
console.warn(`SafePassage SDK: Unknown environment '${normalizedEnvironment}', defaulting to 'production'`);
|
|
71
71
|
normalizedEnvironment = 'production';
|
|
72
72
|
}
|
|
73
|
-
this.config = {
|
|
74
|
-
...config,
|
|
75
|
-
environment: normalizedEnvironment,
|
|
76
|
-
mode: config.mode || 'redirect',
|
|
77
|
-
};
|
|
73
|
+
this.config = Object.assign(Object.assign({}, config), { environment: normalizedEnvironment, mode: config.mode || 'redirect' });
|
|
78
74
|
// Comprehensive environment security validation
|
|
79
75
|
validateEnvironmentSecurity(this.config.environment);
|
|
80
76
|
// Enforce HTTPS in production (additional layer)
|
|
@@ -109,6 +105,7 @@ export class SafePassage {
|
|
|
109
105
|
* @throws {Error} If verification cannot be started or is already in progress
|
|
110
106
|
*/
|
|
111
107
|
async verify(options = {}) {
|
|
108
|
+
var _a, _b, _c, _d, _e, _f;
|
|
112
109
|
const isPublicKey = this.isPublicKey();
|
|
113
110
|
// For public keys, create session via API (SafePassage generates the sessionId)
|
|
114
111
|
let sessionId;
|
|
@@ -125,13 +122,13 @@ export class SafePassage {
|
|
|
125
122
|
}
|
|
126
123
|
// Race condition check - prevent multiple simultaneous verifications
|
|
127
124
|
if (this.isVerificationInProgress) {
|
|
128
|
-
const error = new Error(`Verification already in progress for session ${this.currentSessionId
|
|
125
|
+
const error = new Error(`Verification already in progress for session ${(_a = this.currentSessionId) === null || _a === void 0 ? void 0 : _a.substring(0, 8)}...`);
|
|
129
126
|
logSecurityEvent('RACE_CONDITION_PREVENTED', {
|
|
130
|
-
currentSession: this.currentSessionId
|
|
127
|
+
currentSession: ((_b = this.currentSessionId) === null || _b === void 0 ? void 0 : _b.substring(0, 8)) + '...',
|
|
131
128
|
attemptedSession: 'new-session-attempt',
|
|
132
129
|
origin: window.location.origin,
|
|
133
130
|
});
|
|
134
|
-
this.config.onError
|
|
131
|
+
(_d = (_c = this.config).onError) === null || _d === void 0 ? void 0 : _d.call(_c, error);
|
|
135
132
|
throw error;
|
|
136
133
|
}
|
|
137
134
|
// Lock verification process
|
|
@@ -149,7 +146,7 @@ export class SafePassage {
|
|
|
149
146
|
? sessionId.substring(0, 8) + '...'
|
|
150
147
|
: 'undefined',
|
|
151
148
|
});
|
|
152
|
-
this.config.onError
|
|
149
|
+
(_f = (_e = this.config).onError) === null || _f === void 0 ? void 0 : _f.call(_e, error);
|
|
153
150
|
throw error;
|
|
154
151
|
}
|
|
155
152
|
const verificationUrl = await this.buildVerificationUrl(options, sessionId);
|
|
@@ -224,13 +221,28 @@ export class SafePassage {
|
|
|
224
221
|
const url = new URL(this.lastVerifyUrl);
|
|
225
222
|
url.searchParams.set('state', state);
|
|
226
223
|
url.searchParams.set('mode', this.config.mode);
|
|
224
|
+
// Append skip parameters if provided
|
|
225
|
+
if (options.skipIntro) {
|
|
226
|
+
url.searchParams.set('skip_intro', 'true');
|
|
227
|
+
}
|
|
228
|
+
if (options.autoReturn) {
|
|
229
|
+
url.searchParams.set('auto_return', 'true');
|
|
230
|
+
}
|
|
227
231
|
return url.toString();
|
|
228
232
|
}
|
|
229
|
-
catch {
|
|
233
|
+
catch (_a) {
|
|
230
234
|
// Fall back to client-constructed URL if parsing fails
|
|
231
235
|
}
|
|
232
236
|
}
|
|
237
|
+
// Client-constructed URL fallback (for backwards compatibility)
|
|
233
238
|
const params = new URLSearchParams({ state, sessionId, mode: this.config.mode });
|
|
239
|
+
// Append skip parameters if provided
|
|
240
|
+
if (options.skipIntro) {
|
|
241
|
+
params.set('skip_intro', 'true');
|
|
242
|
+
}
|
|
243
|
+
if (options.autoReturn) {
|
|
244
|
+
params.set('auto_return', 'true');
|
|
245
|
+
}
|
|
234
246
|
return `${baseUrl}/?${params.toString()}`;
|
|
235
247
|
}
|
|
236
248
|
/**
|
|
@@ -257,6 +269,7 @@ export class SafePassage {
|
|
|
257
269
|
* @private
|
|
258
270
|
*/
|
|
259
271
|
openNewTab(url, sessionId) {
|
|
272
|
+
var _a, _b;
|
|
260
273
|
// Clean up any existing resources
|
|
261
274
|
this.cleanup();
|
|
262
275
|
// Clear any existing popup monitor interval
|
|
@@ -267,18 +280,19 @@ export class SafePassage {
|
|
|
267
280
|
// Open new tab
|
|
268
281
|
this.popupWindow = window.open(url, 'safepassage-verify', 'width=600,height=700');
|
|
269
282
|
if (!this.popupWindow) {
|
|
270
|
-
this.config.onError
|
|
283
|
+
(_b = (_a = this.config).onError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error('Failed to open verification window. Please check popup blocker settings.'));
|
|
271
284
|
return;
|
|
272
285
|
}
|
|
273
286
|
// Set up PostMessage listener with enhanced security
|
|
274
287
|
this.messageListener = (event) => {
|
|
288
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
275
289
|
// Enhanced origin validation with strict allowlist
|
|
276
290
|
if (!validatePostMessageOrigin(event, this.config.environment)) {
|
|
277
291
|
logSecurityEvent('POSTMESSAGE_ORIGIN_BLOCKED', {
|
|
278
292
|
origin: event.origin,
|
|
279
293
|
environment: this.config.environment,
|
|
280
294
|
expectedOrigins: `SafePassage trusted origins for ${this.config.environment}`,
|
|
281
|
-
messageType: event.data
|
|
295
|
+
messageType: (_a = event.data) === null || _a === void 0 ? void 0 : _a.type,
|
|
282
296
|
});
|
|
283
297
|
return;
|
|
284
298
|
}
|
|
@@ -289,7 +303,7 @@ export class SafePassage {
|
|
|
289
303
|
error: messageValidation.error,
|
|
290
304
|
origin: event.origin,
|
|
291
305
|
sessionId: sessionId.substring(0, 8) + '...',
|
|
292
|
-
messageType: event.data
|
|
306
|
+
messageType: (_b = event.data) === null || _b === void 0 ? void 0 : _b.type,
|
|
293
307
|
});
|
|
294
308
|
return;
|
|
295
309
|
}
|
|
@@ -314,18 +328,19 @@ export class SafePassage {
|
|
|
314
328
|
}
|
|
315
329
|
// Trigger appropriate callback
|
|
316
330
|
if (result.status === 'verified') {
|
|
317
|
-
this.config.onComplete
|
|
331
|
+
(_d = (_c = this.config).onComplete) === null || _d === void 0 ? void 0 : _d.call(_c, result);
|
|
318
332
|
}
|
|
319
333
|
else if (result.status === 'cancelled') {
|
|
320
|
-
this.config.onCancel
|
|
334
|
+
(_f = (_e = this.config).onCancel) === null || _f === void 0 ? void 0 : _f.call(_e);
|
|
321
335
|
}
|
|
322
336
|
else {
|
|
323
|
-
this.config.onError
|
|
337
|
+
(_h = (_g = this.config).onError) === null || _h === void 0 ? void 0 : _h.call(_g, new Error(`Verification failed: ${result.status}`));
|
|
324
338
|
}
|
|
325
339
|
};
|
|
326
340
|
window.addEventListener('message', this.messageListener);
|
|
327
341
|
// Monitor popup window with proper cleanup
|
|
328
342
|
this.popupMonitorInterval = setInterval(() => {
|
|
343
|
+
var _a, _b;
|
|
329
344
|
if (this.popupWindow && this.popupWindow.closed) {
|
|
330
345
|
// Log popup closed event
|
|
331
346
|
logSecurityEvent('POPUP_CLOSED_BY_USER', {
|
|
@@ -336,7 +351,7 @@ export class SafePassage {
|
|
|
336
351
|
this.cleanup();
|
|
337
352
|
// Unlock verification after popup closed
|
|
338
353
|
this.unlockVerification();
|
|
339
|
-
this.config.onCancel
|
|
354
|
+
(_b = (_a = this.config).onCancel) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
340
355
|
}
|
|
341
356
|
}, 500);
|
|
342
357
|
}
|
|
@@ -556,6 +571,7 @@ export class SafePassage {
|
|
|
556
571
|
* @private
|
|
557
572
|
*/
|
|
558
573
|
async createInternalSession(options) {
|
|
574
|
+
var _a, _b;
|
|
559
575
|
try {
|
|
560
576
|
const portalApiUrl = this.getPortalApiUrl();
|
|
561
577
|
const response = await fetch(`${portalApiUrl}/api/v1/sessions/create`, {
|
|
@@ -612,7 +628,7 @@ export class SafePassage {
|
|
|
612
628
|
environment: this.config.environment,
|
|
613
629
|
apiKeyType: 'public',
|
|
614
630
|
});
|
|
615
|
-
this.config.onError
|
|
631
|
+
(_b = (_a = this.config).onError) === null || _b === void 0 ? void 0 : _b.call(_a, error);
|
|
616
632
|
throw new Error(`Failed to create verification session: ${errorMessage}`);
|
|
617
633
|
}
|
|
618
634
|
}
|
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.4.0";
|
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 -
|
|
28
|
-
export const VERSION = '3.0
|
|
27
|
+
// Version - 3.4.0: Deprecated cancelUrl (all redirects now use returnUrl with status param)
|
|
28
|
+
export const VERSION = '3.4.0';
|
|
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
1
|
/* SafePassage SDK v3.0.15 - Redirect Implementation with Enhanced Security */
|
|
2
|
-
"use strict";var SafePassageSDK=(()=>{var f=Object.defineProperty;var X=Object.getOwnPropertyDescriptor;var Y=Object.getOwnPropertyNames;var z=Object.prototype.hasOwnProperty;var p=(t,e)=>()=>(t&&(e=t(t=0)),e);var h=(t,e)=>{for(var n in e)f(t,n,{get:e[n],enumerable:!0})},Z=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of Y(e))!z.call(t,r)&&r!==n&&f(t,r,{get:()=>e[r],enumerable:!(i=X(e,r))||i.enumerable});return t};var A=t=>Z(f({},"__esModule",{value:!0}),t);function Q(t,e){return x[e].includes(t)}function V(t,e,n=[]){let{origin:i}=t;return Q(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:x[e],allowedCustomOrigins:n,eventType:t.data?.type}),!1)}function k(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 M(t){t==="production"&&window.location.protocol!=="https:"&&console.warn("SafePassage Warning: HTTPS recommended for production environment",{current:window.location.href})}function w(t,e){try{let n=new URL(t);if(n.protocol!=="https:"&&!(n.hostname==="localhost"||n.hostname==="127.0.0.1"))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 a(t,e){console.warn(`SafePassage Security Event: ${t}`,{timestamp:new Date().toISOString(),userAgent:navigator.userAgent,url:window.location.href,...e})}var x,m,L,v=p(()=>{"use strict";x={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"]};m=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)}},L=new m});var R={};h(R,{createSignedState:()=>te,generateHMAC:()=>S,generateSecureToken:()=>C,getSigningSecret:()=>y,parseSignedState:()=>ne,verifyHMAC:()=>_});async function S(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 _(t,e,n){try{let i=await S(t,n);return ee(e,i)}catch{return!1}}function ee(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 y(t){return{production:"safepassage-prod-hmac-2025",staging:"safepassage-stage-hmac-2025"}[t]}async function te(t,e){let n={...t,timestamp:Date.now(),nonce:C(16)},i=JSON.stringify(n),r=y(e),s=await S(i,r);return btoa(JSON.stringify({data:n,signature:s}))}async function ne(t,e,n=$){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),u=y(e);if(!await _(l,o,u))return console.warn("SafePassage: State signature verification failed"),null;if(s.timestamp){let U=Date.now()-s.timestamp;if(U>n)return console.warn("SafePassage: State parameter expired",{age:U,maxAge:n}),null}let{timestamp:ce,nonce:le,...J}=s;return J}catch(i){return console.warn("SafePassage: Failed to parse signed state",i),null}}var D=p(()=>{"use strict";E()});function W(t){if(!t.apiKey)throw new Error("apiKey is required");if(t.apiKey.length>K)throw new Error(`apiKey exceeds maximum length of ${K} characters`);if(!ie.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>g)throw new Error(`returnUrl exceeds maximum length of ${g} characters`);if(!t.cancelUrl)throw new Error("cancelUrl is required");if(t.cancelUrl.length>g)throw new Error(`cancelUrl exceeds maximum length of ${g} characters`);let e=re(),n=w(t.returnUrl,e);if(!n.isValid)throw new Error(`returnUrl validation failed: ${n.error}`);let i=w(t.cancelUrl,e);if(!i.isValid)throw new Error(`cancelUrl validation failed: ${i.error}`);if(t.defaultChallengeAge!==void 0){if(t.defaultChallengeAge<O)throw new Error(`defaultChallengeAge must be at least ${O}`);if(t.defaultChallengeAge>N)throw new Error(`defaultChallengeAge cannot exceed ${N}`)}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 re(){if(typeof window>"u")return"production";let t=window.location.hostname;return t.includes("staging")||t.includes("stage")?"staging":"production"}async function H(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(D(),R));return n(t,e)}var O,N,g,K,$,ie,E=p(()=>{"use strict";v();O=25,N=150,g=2048,K=128,$=6e5,ie=/^(pk_|sk_)[a-zA-Z0-9_]+$/});function I(t){let e=q[t]||q.production;if(!e||!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function se(t){let e={production:"https://api.safepassageapp.com",staging:"https://api.staging.safepassageapp.com"},n=e[t]||e.production;if(!n||!n.startsWith("https://"))throw new Error(`HTTPS required for API URLs in ${t} environment`);return n}function j(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{I(t),se(t)}catch(n){let i=n instanceof Error?n.message:String(n);throw new Error(`Environment configuration validation failed: ${i}`)}}var q,F=p(()=>{"use strict";q={production:"https://av.safepassageapp.com",staging:"https://av.staging.safepassageapp.com"}});var B={};h(B,{SafePassage:()=>c,default:()=>oe});var c,oe,P=p(()=>{"use strict";E();F();v();c=class{constructor(e){this.popupWindow=null;this.messageListener=null;this.popupMonitorInterval=null;this.unloadListener=null;this.isVerificationInProgress=!1;this.currentSessionId=null;this.lastVerifyUrl=null;this.lastSessionToken=null;W(e);let n=e.environment||this.detectEnvironment();n!=="staging"&&n!=="production"&&(console.warn(`SafePassage SDK: Unknown environment '${n}', defaulting to 'production'`),n="production"),this.config={...e,environment:n,mode:e.mode||"redirect"},j(this.config.environment),M(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;if(n)i=await this.createInternalSession(e);else throw new Error("Private API keys (sk_) should use the direct API, not the SDK. The SDK is designed for browser-based public key usage only.");if(!i)throw new Error("Failed to obtain sessionId from server");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:"new-session-attempt",origin:window.location.origin}),this.config.onError?.(r),r}this.isVerificationInProgress=!0,this.currentSessionId=i;try{let r=`${this.config.apiKey}:${window.location.origin}`;if(!L.isAllowed(r)){let 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,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,n){let i=I(this.config.environment),r=e.challengeAge!==void 0,s=e.verificationMode!==void 0,o=r||s,l=await H({merchantId:this.config.apiKey,sessionId:n,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,sessionToken:this.lastSessionToken||void 0,verifyUrl:this.lastVerifyUrl||void 0},this.config.environment);if(this.lastVerifyUrl)try{let d=new URL(this.lastVerifyUrl);return d.searchParams.set("state",l),d.searchParams.set("mode",this.config.mode),d.toString()}catch{}let u=new URLSearchParams({state:l,sessionId:n,mode:this.config.mode});return`${i}/?${u.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(!V(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=k(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){try{let n=this.getPortalApiUrl(),i=await fetch(`${n}/api/v1/sessions/create`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({merchantId:this.config.apiKey,returnUrl:this.config.returnUrl,cancelUrl:this.config.cancelUrl,challengeAge:e.challengeAge,verificationMode:e.verificationMode,merchantName:document.title||window.location.hostname,externalUserId:e.externalUserId})});if(!i.ok){let o=await i.json().catch(()=>({}));throw new Error(`Failed to create session: ${i.status} ${i.statusText}. ${o.message||""}`)}let r=await i.json(),s=r.sessionId;if(!s)throw new Error("Server did not return a sessionId");return r.verifyUrl&&(this.lastVerifyUrl=r.verifyUrl),r.sessionToken&&(this.lastSessionToken=r.sessionToken),r.handoffToken&&(this._temporaryHandoffToken=r.handoffToken),a("INTERNAL_SESSION_CREATED",{sessionId:s.substring(0,8)+"...",environment:this.config.environment,apiKeyType:"public"}),s}catch(n){let i=n instanceof Error?n.message:String(n);throw a("INTERNAL_SESSION_FAILED",{error:i,environment:this.config.environment,apiKeyType:"public"}),this.config.onError?.(n),new Error(`Failed to create verification session: ${i}`)}}},oe=c});var ae={};h(ae,{SafePassage:()=>c,VERSION:()=>G,default:()=>c});function b(){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 T(){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("; "))}P();typeof window<"u"&&(b(),T());var G="3.0.15";if(typeof window<"u"&&window){let{SafePassage:t}=(P(),A(B)),e=window;e.SafePassage=t,e.SafePassage&&(e.SafePassage.VERSION=G)}return A(ae);})();
|
|
2
|
+
"use strict";var SafePassageSDK=(()=>{var S=Object.defineProperty,oe=Object.defineProperties,ae=Object.getOwnPropertyDescriptor,ce=Object.getOwnPropertyDescriptors,le=Object.getOwnPropertyNames,v=Object.getOwnPropertySymbols;var P=Object.prototype.hasOwnProperty,C=Object.prototype.propertyIsEnumerable;var R=(t,e,n)=>e in t?S(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,h=(t,e)=>{for(var n in e||(e={}))P.call(e,n)&&R(t,n,e[n]);if(v)for(var n of v(e))C.call(e,n)&&R(t,n,e[n]);return t},y=(t,e)=>oe(t,ce(e));var D=(t,e)=>{var n={};for(var i in t)P.call(t,i)&&e.indexOf(i)<0&&(n[i]=t[i]);if(t!=null&&v)for(var i of v(t))e.indexOf(i)<0&&C.call(t,i)&&(n[i]=t[i]);return n};var w=(t,e)=>()=>(t&&(e=t(t=0)),e);var U=(t,e)=>{for(var n in e)S(t,n,{get:e[n],enumerable:!0})},pe=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of le(e))!P.call(t,r)&&r!==n&&S(t,r,{get:()=>e[r],enumerable:!(i=ae(e,r))||i.enumerable});return t};var $=t=>pe(S({},"__esModule",{value:!0}),t);function de(t,e){return K[e].includes(t)}function W(t,e,n=[]){var r;let{origin:i}=t;return de(i,e)||n.length>0&&n.some(o=>{if(o.startsWith("*.")){let a=o.slice(2);return i.endsWith(`.${a}`)||i===`https://${a}`||i===`http://${a}`}return i===o})?!0:(console.warn(`SafePassage Security: Blocked PostMessage from untrusted origin: ${i}`,{environment:e,trustedOrigins:K[e],allowedCustomOrigins:n,eventType:(r=t.data)==null?void 0:r.type}),!1)}function H(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 j(t){t==="production"&&window.location.protocol!=="https:"&&console.warn("SafePassage Warning: HTTPS recommended for production environment",{current:window.location.href})}function T(t,e){try{let n=new URL(t);if(n.protocol!=="https:"&&!(n.hostname==="localhost"||n.hostname==="127.0.0.1"))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(n){return{isValid:!1,error:"Invalid URL format"}}}function l(t,e){console.warn(`SafePassage Security Event: ${t}`,h({timestamp:new Date().toISOString(),userAgent:navigator.userAgent,url:window.location.href},e))}var K,A,q,b=w(()=>{"use strict";K={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"]};A=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)}},q=new A});var G={};U(G,{createSignedState:()=>ge,generateHMAC:()=>x,generateSecureToken:()=>B,getSigningSecret:()=>V,parseSignedState:()=>fe,verifyHMAC:()=>F});async function x(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(a=>a.toString(16).padStart(2,"0")).join("")}async function F(t,e,n){try{let i=await x(t,n);return ue(e,i)}catch(i){return!1}}function ue(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 V(t){return{production:"safepassage-prod-hmac-2025",staging:"safepassage-stage-hmac-2025"}[t]}async function ge(t,e){let n=y(h({},t),{timestamp:Date.now(),nonce:B(16)}),i=JSON.stringify(n),r=V(e),s=await x(i,r);return btoa(JSON.stringify({data:n,signature:s}))}async function fe(t,e,n=X){try{let r=atob(t),s=JSON.parse(r);if(!s.data||!s.signature)return console.warn("SafePassage: Invalid signed state format"),null;let{data:o,signature:a}=s,p=JSON.stringify(o),c=V(e);if(!await F(p,a,c))return console.warn("SafePassage: State signature verification failed"),null;if(o.timestamp){let m=Date.now()-o.timestamp;if(m>n)return console.warn("SafePassage: State parameter expired",{age:m,maxAge:n}),null}let i=o,{timestamp:g,nonce:f}=i;return D(i,["timestamp","nonce"])}catch(r){return console.warn("SafePassage: Failed to parse signed state",r),null}}var J=w(()=>{"use strict";k()});function Q(t){if(!t.apiKey)throw new Error("apiKey is required");if(t.apiKey.length>Z)throw new Error(`apiKey exceeds maximum length of ${Z} characters`);if(!he.test(t.apiKey))throw new Error("Invalid apiKey format. Expected pk_xxx (public) or sk_xxx (private)");if(typeof window!="undefined"&&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>E)throw new Error(`returnUrl exceeds maximum length of ${E} characters`);let e=me(),n=T(t.returnUrl,e);if(!n.isValid)throw new Error(`returnUrl validation failed: ${n.error}`);if(t.cancelUrl){if(t.cancelUrl.length>E)throw new Error(`cancelUrl exceeds maximum length of ${E} characters`);let i=T(t.cancelUrl,e);if(!i.isValid)throw new Error(`cancelUrl validation failed: ${i.error}`)}if(t.defaultChallengeAge!==void 0){if(t.defaultChallengeAge<Y)throw new Error(`defaultChallengeAge must be at least ${Y}`);if(t.defaultChallengeAge>z)throw new Error(`defaultChallengeAge cannot exceed ${z}`)}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 me(){if(typeof window=="undefined")return"production";let t=window.location.hostname;return t.includes("staging")||t.includes("stage")?"staging":"production"}async function ee(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(J(),G));return n(t,e)}var Y,z,E,Z,X,he,k=w(()=>{"use strict";b();Y=25,z=150,E=2048,Z=128,X=6e5,he=/^(pk_|sk_)[a-zA-Z0-9_]+$/});function M(t){let e=te[t]||te.production;if(!e||!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function we(t){let e={production:"https://api.safepassageapp.com",staging:"https://api.staging.safepassageapp.com"},n=e[t]||e.production;if(!n||!n.startsWith("https://"))throw new Error(`HTTPS required for API URLs in ${t} environment`);return n}function ne(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{M(t),we(t)}catch(n){let i=n instanceof Error?n.message:String(n);throw new Error(`Environment configuration validation failed: ${i}`)}}var te,ie=w(()=>{"use strict";te={production:"https://av.safepassageapp.com",staging:"https://av.staging.safepassageapp.com"}});var re={};U(re,{SafePassage:()=>u,default:()=>ve});var u,ve,_=w(()=>{"use strict";k();ie();b();u=class{constructor(e){this.popupWindow=null;this.messageListener=null;this.popupMonitorInterval=null;this.unloadListener=null;this.isVerificationInProgress=!1;this.currentSessionId=null;this.lastVerifyUrl=null;this.lastSessionToken=null;Q(e);let n=e.environment||this.detectEnvironment();n!=="staging"&&n!=="production"&&(console.warn(`SafePassage SDK: Unknown environment '${n}', defaulting to 'production'`),n="production"),this.config=y(h({},e),{environment:n,mode:e.mode||"redirect"}),ne(this.config.environment),j(this.config.environment),l("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={}){var r,s,o,a,p,c;let n=this.isPublicKey(),i;if(n)i=await this.createInternalSession(e);else throw new Error("Private API keys (sk_) should use the direct API, not the SDK. The SDK is designed for browser-based public key usage only.");if(!i)throw new Error("Failed to obtain sessionId from server");if(this.isVerificationInProgress){let d=new Error(`Verification already in progress for session ${(r=this.currentSessionId)==null?void 0:r.substring(0,8)}...`);throw l("RACE_CONDITION_PREVENTED",{currentSession:((s=this.currentSessionId)==null?void 0:s.substring(0,8))+"...",attemptedSession:"new-session-attempt",origin:window.location.origin}),(a=(o=this.config).onError)==null||a.call(o,d),d}this.isVerificationInProgress=!0,this.currentSessionId=i;try{let d=`${this.config.apiKey}:${window.location.origin}`;if(!q.isAllowed(d)){let f=new Error("Too many verification attempts. Please wait before trying again.");throw l("RATE_LIMIT_EXCEEDED",{apiKey:this.config.apiKey.substring(0,8)+"...",origin:window.location.origin,sessionId:i?i.substring(0,8)+"...":"undefined"}),(c=(p=this.config).onError)==null||c.call(p,f),f}let g=await this.buildVerificationUrl(e,i);l("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(g,i):(this.unlockVerification(),this.redirect(g))}catch(d){throw this.unlockVerification(),d}}async buildVerificationUrl(e,n){let i=M(this.config.environment),r=e.challengeAge!==void 0,s=e.verificationMode!==void 0,o=r||s,a=await ee({merchantId:this.config.apiKey,sessionId:n,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,sessionToken:this.lastSessionToken||void 0,verifyUrl:this.lastVerifyUrl||void 0},this.config.environment);if(this.lastVerifyUrl)try{let c=new URL(this.lastVerifyUrl);return c.searchParams.set("state",a),c.searchParams.set("mode",this.config.mode),e.skipIntro&&c.searchParams.set("skip_intro","true"),e.autoReturn&&c.searchParams.set("auto_return","true"),c.toString()}catch(c){}let p=new URLSearchParams({state:a,sessionId:n,mode:this.config.mode});return e.skipIntro&&p.set("skip_intro","true"),e.autoReturn&&p.set("auto_return","true"),`${i}/?${p.toString()}`}redirect(e){window.location.href=e}openNewTab(e,n){var i,r;if(this.cleanup(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),this.popupWindow=window.open(e,"safepassage-verify","width=600,height=700"),!this.popupWindow){(r=(i=this.config).onError)==null||r.call(i,new Error("Failed to open verification window. Please check popup blocker settings."));return}this.messageListener=s=>{var p,c,d,g,f,I,m,L;if(!W(s,this.config.environment)){l("POSTMESSAGE_ORIGIN_BLOCKED",{origin:s.origin,environment:this.config.environment,expectedOrigins:`SafePassage trusted origins for ${this.config.environment}`,messageType:(p=s.data)==null?void 0:p.type});return}let o=H(s,n);if(!o.isValid){l("POSTMESSAGE_VALIDATION_FAILED",{error:o.error,origin:s.origin,sessionId:n.substring(0,8)+"...",messageType:(c=s.data)==null?void 0:c.type});return}let a={sessionId:s.data.sessionId,status:s.data.status};l("VERIFICATION_COMPLETED",{status:a.status,sessionId:n.substring(0,8)+"...",origin:s.origin}),this.cleanup(),this.unlockVerification(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),a.status==="verified"?(g=(d=this.config).onComplete)==null||g.call(d,a):a.status==="cancelled"?(I=(f=this.config).onCancel)==null||I.call(f):(L=(m=this.config).onError)==null||L.call(m,new Error(`Verification failed: ${a.status}`))},window.addEventListener("message",this.messageListener),this.popupMonitorInterval=setInterval(()=>{var s,o;this.popupWindow&&this.popupWindow.closed&&(l("POPUP_CLOSED_BY_USER",{sessionId:n.substring(0,8)+"...",environment:this.config.environment}),this.cleanup(),this.unlockVerification(),(o=(s=this.config).onCancel)==null||o.call(s))},500)}setupAutoCleanup(){if(this.unloadListener=()=>{l("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,l("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(){l("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){var n,i;try{let r=this.getPortalApiUrl(),s=await fetch(`${r}/api/v1/sessions/create`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({merchantId:this.config.apiKey,returnUrl:this.config.returnUrl,cancelUrl:this.config.cancelUrl,challengeAge:e.challengeAge,verificationMode:e.verificationMode,merchantName:document.title||window.location.hostname,externalUserId:e.externalUserId})});if(!s.ok){let p=await s.json().catch(()=>({}));throw new Error(`Failed to create session: ${s.status} ${s.statusText}. ${p.message||""}`)}let o=await s.json(),a=o.sessionId;if(!a)throw new Error("Server did not return a sessionId");return o.verifyUrl&&(this.lastVerifyUrl=o.verifyUrl),o.sessionToken&&(this.lastSessionToken=o.sessionToken),o.handoffToken&&(this._temporaryHandoffToken=o.handoffToken),l("INTERNAL_SESSION_CREATED",{sessionId:a.substring(0,8)+"...",environment:this.config.environment,apiKeyType:"public"}),a}catch(r){let s=r instanceof Error?r.message:String(r);throw l("INTERNAL_SESSION_FAILED",{error:s,environment:this.config.environment,apiKeyType:"public"}),(i=(n=this.config).onError)==null||i.call(n,r),new Error(`Failed to create verification session: ${s}`)}}},ve=u});var Se={};U(Se,{SafePassage:()=>u,VERSION:()=>se,default:()=>u});function O(){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 N(){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"),t.length>0&&console.warn("SafePassage SDK Browser Compatibility:",t.join("; "))}_();typeof window!="undefined"&&(O(),N());var se="3.4.0";if(typeof window!="undefined"&&window){let{SafePassage:t}=(_(),$(re)),e=window;e.SafePassage=t,e.SafePassage&&(e.SafePassage.VERSION=se)}return $(Se);})();
|
|
3
3
|
if(typeof SafePassageSDK !== "undefined" && SafePassageSDK.SafePassage) { window.SafePassage = SafePassageSDK.SafePassage; window.SafePassage.VERSION = SafePassageSDK.VERSION; }
|
package/dist/types/index.d.ts
CHANGED
|
@@ -13,9 +13,10 @@ export interface SafePassageConfig {
|
|
|
13
13
|
returnUrl: string;
|
|
14
14
|
/**
|
|
15
15
|
* URL to redirect to if user cancels verification
|
|
16
|
-
*
|
|
16
|
+
* @deprecated This field is no longer used. All redirects go to returnUrl with a status parameter.
|
|
17
|
+
* Kept for backwards compatibility but will be ignored.
|
|
17
18
|
*/
|
|
18
|
-
cancelUrl
|
|
19
|
+
cancelUrl?: string;
|
|
19
20
|
/**
|
|
20
21
|
* Environment to use
|
|
21
22
|
* @default Auto-detected based on hostname (production unless staging detected)
|
|
@@ -68,6 +69,18 @@ export interface VerificationOptions {
|
|
|
68
69
|
* Useful for correlating SafePassage sessions with merchant user records
|
|
69
70
|
*/
|
|
70
71
|
externalUserId?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Skip the intro screen and go directly to camera access
|
|
74
|
+
* Useful for embedded/streamlined flows where user has already consented
|
|
75
|
+
* @default false
|
|
76
|
+
*/
|
|
77
|
+
skipIntro?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Automatically redirect to returnUrl immediately after successful verification
|
|
80
|
+
* Skips the success screen for seamless embedded experiences
|
|
81
|
+
* @default false
|
|
82
|
+
*/
|
|
83
|
+
autoReturn?: boolean;
|
|
71
84
|
}
|
|
72
85
|
export interface VerificationResult {
|
|
73
86
|
/**
|
|
@@ -88,7 +101,8 @@ export interface StatePayload {
|
|
|
88
101
|
merchantId: string;
|
|
89
102
|
sessionId: string;
|
|
90
103
|
returnUrl: string;
|
|
91
|
-
|
|
104
|
+
/** @deprecated No longer used - all redirects use returnUrl with status parameter */
|
|
105
|
+
cancelUrl?: string;
|
|
92
106
|
challengeAge?: number;
|
|
93
107
|
verificationMode?: 'L1' | 'L2';
|
|
94
108
|
hasOverrides?: boolean;
|
package/dist/utils/crypto.js
CHANGED
|
@@ -22,6 +22,17 @@
|
|
|
22
22
|
* @author SafePassage Engineering
|
|
23
23
|
* @version 1.0.0
|
|
24
24
|
*/
|
|
25
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
26
|
+
var t = {};
|
|
27
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
28
|
+
t[p] = s[p];
|
|
29
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
30
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
31
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
32
|
+
t[p[i]] = s[p[i]];
|
|
33
|
+
}
|
|
34
|
+
return t;
|
|
35
|
+
};
|
|
25
36
|
import { STATE_EXPIRY_MS } from './validation';
|
|
26
37
|
/**
|
|
27
38
|
* Generate HMAC-SHA256 signature using Web Crypto API
|
|
@@ -69,7 +80,7 @@ export async function verifyHMAC(data, signature, secret) {
|
|
|
69
80
|
const expectedSignature = await generateHMAC(data, secret);
|
|
70
81
|
return constantTimeCompare(signature, expectedSignature);
|
|
71
82
|
}
|
|
72
|
-
catch {
|
|
83
|
+
catch (_a) {
|
|
73
84
|
return false;
|
|
74
85
|
}
|
|
75
86
|
}
|
|
@@ -145,11 +156,7 @@ export function getSigningSecret(environment) {
|
|
|
145
156
|
*/
|
|
146
157
|
export async function createSignedState(payload, environment) {
|
|
147
158
|
// Add timestamp for freshness
|
|
148
|
-
const timestampedPayload = {
|
|
149
|
-
...payload,
|
|
150
|
-
timestamp: Date.now(),
|
|
151
|
-
nonce: generateSecureToken(16), // Add nonce to prevent replay attacks
|
|
152
|
-
};
|
|
159
|
+
const timestampedPayload = Object.assign(Object.assign({}, payload), { timestamp: Date.now(), nonce: generateSecureToken(16) });
|
|
153
160
|
const dataString = JSON.stringify(timestampedPayload);
|
|
154
161
|
const secret = getSigningSecret(environment);
|
|
155
162
|
const signature = await generateHMAC(dataString, secret);
|
|
@@ -199,7 +206,7 @@ export async function parseSignedState(signedState, environment, maxAge = STATE_
|
|
|
199
206
|
}
|
|
200
207
|
// Remove internal fields before returning
|
|
201
208
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
202
|
-
const { timestamp, nonce,
|
|
209
|
+
const { timestamp, nonce } = data, payload = __rest(data, ["timestamp", "nonce"]);
|
|
203
210
|
return payload;
|
|
204
211
|
}
|
|
205
212
|
catch (error) {
|
package/dist/utils/polyfills.js
CHANGED
|
@@ -51,17 +51,6 @@ export function checkBrowserCompatibility() {
|
|
|
51
51
|
if (!window.URLSearchParams) {
|
|
52
52
|
warnings.push('URLSearchParams not supported, consider adding a polyfill for IE 11 support');
|
|
53
53
|
}
|
|
54
|
-
// Check for modern JavaScript features
|
|
55
|
-
try {
|
|
56
|
-
// Test optional chaining
|
|
57
|
-
const test = { a: { b: 1 } };
|
|
58
|
-
const result = test?.a?.b;
|
|
59
|
-
if (result !== 1)
|
|
60
|
-
throw new Error();
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
warnings.push('Optional chaining (?.) not supported, ensure transpilation for older browsers');
|
|
64
|
-
}
|
|
65
54
|
// Log warnings if any
|
|
66
55
|
if (warnings.length > 0) {
|
|
67
56
|
console.warn('SafePassage SDK Browser Compatibility:', warnings.join('; '));
|
package/dist/utils/security.js
CHANGED
|
@@ -29,6 +29,7 @@ export function isOriginTrusted(origin, environment) {
|
|
|
29
29
|
* Enhanced origin validation with logging and strict allowlist
|
|
30
30
|
*/
|
|
31
31
|
export function validatePostMessageOrigin(event, environment, allowedCustomOrigins = []) {
|
|
32
|
+
var _a;
|
|
32
33
|
const { origin } = event;
|
|
33
34
|
// Check against trusted SafePassage origins
|
|
34
35
|
if (isOriginTrusted(origin, environment)) {
|
|
@@ -55,7 +56,7 @@ export function validatePostMessageOrigin(event, environment, allowedCustomOrigi
|
|
|
55
56
|
environment,
|
|
56
57
|
trustedOrigins: TRUSTED_ORIGINS[environment],
|
|
57
58
|
allowedCustomOrigins,
|
|
58
|
-
eventType: event.data
|
|
59
|
+
eventType: (_a = event.data) === null || _a === void 0 ? void 0 : _a.type,
|
|
59
60
|
});
|
|
60
61
|
return false;
|
|
61
62
|
}
|
|
@@ -125,7 +126,7 @@ export function validateReturnUrl(url, environment) {
|
|
|
125
126
|
}
|
|
126
127
|
return { isValid: true };
|
|
127
128
|
}
|
|
128
|
-
catch {
|
|
129
|
+
catch (_a) {
|
|
129
130
|
return { isValid: false, error: 'Invalid URL format' };
|
|
130
131
|
}
|
|
131
132
|
}
|
|
@@ -185,11 +186,6 @@ export const verificationRateLimit = new VerificationRateLimit();
|
|
|
185
186
|
* Security event logging for monitoring
|
|
186
187
|
*/
|
|
187
188
|
export function logSecurityEvent(event, details) {
|
|
188
|
-
console.warn(`SafePassage Security Event: ${event}`, {
|
|
189
|
-
timestamp: new Date().toISOString(),
|
|
190
|
-
userAgent: navigator.userAgent,
|
|
191
|
-
url: window.location.href,
|
|
192
|
-
...details,
|
|
193
|
-
});
|
|
189
|
+
console.warn(`SafePassage Security Event: ${event}`, Object.assign({ timestamp: new Date().toISOString(), userAgent: navigator.userAgent, url: window.location.href }, details));
|
|
194
190
|
// In production, this could send events to a security monitoring service
|
|
195
191
|
}
|
package/dist/utils/validation.js
CHANGED
|
@@ -35,21 +35,21 @@ export function validateConfig(config) {
|
|
|
35
35
|
if (config.returnUrl.length > MAX_URL_LENGTH) {
|
|
36
36
|
throw new Error(`returnUrl exceeds maximum length of ${MAX_URL_LENGTH} characters`);
|
|
37
37
|
}
|
|
38
|
-
if (!config.cancelUrl) {
|
|
39
|
-
throw new Error('cancelUrl is required');
|
|
40
|
-
}
|
|
41
|
-
if (config.cancelUrl.length > MAX_URL_LENGTH) {
|
|
42
|
-
throw new Error(`cancelUrl exceeds maximum length of ${MAX_URL_LENGTH} characters`);
|
|
43
|
-
}
|
|
44
38
|
// Enhanced URL validation with security checks
|
|
45
39
|
const environment = detectEnvironment();
|
|
46
40
|
const returnUrlValidation = validateReturnUrl(config.returnUrl, environment);
|
|
47
41
|
if (!returnUrlValidation.isValid) {
|
|
48
42
|
throw new Error(`returnUrl validation failed: ${returnUrlValidation.error}`);
|
|
49
43
|
}
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
44
|
+
// cancelUrl is deprecated but still validate if provided for backwards compatibility
|
|
45
|
+
if (config.cancelUrl) {
|
|
46
|
+
if (config.cancelUrl.length > MAX_URL_LENGTH) {
|
|
47
|
+
throw new Error(`cancelUrl exceeds maximum length of ${MAX_URL_LENGTH} characters`);
|
|
48
|
+
}
|
|
49
|
+
const cancelUrlValidation = validateReturnUrl(config.cancelUrl, environment);
|
|
50
|
+
if (!cancelUrlValidation.isValid) {
|
|
51
|
+
throw new Error(`cancelUrl validation failed: ${cancelUrlValidation.error}`);
|
|
52
|
+
}
|
|
53
53
|
}
|
|
54
54
|
if (config.defaultChallengeAge !== undefined) {
|
|
55
55
|
if (config.defaultChallengeAge < MINIMUM_AGE) {
|
|
@@ -121,11 +121,10 @@ export async function parseState(state, environment) {
|
|
|
121
121
|
if (signedPayload) {
|
|
122
122
|
// Type guard to ensure signedPayload has required properties
|
|
123
123
|
const payload = signedPayload;
|
|
124
|
-
// Validate payload structure
|
|
124
|
+
// Validate payload structure (cancelUrl is deprecated and no longer required)
|
|
125
125
|
if (!payload.merchantId ||
|
|
126
126
|
!payload.sessionId ||
|
|
127
|
-
!payload.returnUrl
|
|
128
|
-
!payload.cancelUrl) {
|
|
127
|
+
!payload.returnUrl) {
|
|
129
128
|
return null;
|
|
130
129
|
}
|
|
131
130
|
// Cast to unknown first to satisfy TypeScript's type checking
|
|
@@ -135,11 +134,10 @@ export async function parseState(state, environment) {
|
|
|
135
134
|
console.warn('SafePassage: Falling back to legacy state format - update your SDK');
|
|
136
135
|
const json = atob(state);
|
|
137
136
|
const payload = JSON.parse(json);
|
|
138
|
-
// Validate payload structure
|
|
137
|
+
// Validate payload structure (cancelUrl is deprecated and no longer required)
|
|
139
138
|
if (!payload.merchantId ||
|
|
140
139
|
!payload.sessionId ||
|
|
141
|
-
!payload.returnUrl
|
|
142
|
-
!payload.cancelUrl) {
|
|
140
|
+
!payload.returnUrl) {
|
|
143
141
|
return null;
|
|
144
142
|
}
|
|
145
143
|
// Check timestamp expiration
|