@safepassage/sdk 3.2.3 → 3.3.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 +44 -17
- package/dist/core/SafePassageSDK.d.ts +1 -0
- package/dist/core/SafePassageSDK.js +43 -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 +14 -0
- package/dist/utils/crypto.js +14 -7
- package/dist/utils/polyfills.js +0 -11
- package/dist/utils/security.js +4 -8
- 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)
|
|
@@ -61,6 +61,8 @@ export class SafePassage {
|
|
|
61
61
|
this.currentSessionId = null;
|
|
62
62
|
// Server-provided verify URL (includes sessionToken)
|
|
63
63
|
this.lastVerifyUrl = null;
|
|
64
|
+
// Server-provided session token (WS auth)
|
|
65
|
+
this.lastSessionToken = null;
|
|
64
66
|
validateConfig(config);
|
|
65
67
|
// Normalize environment: treat 'development' or any unknown value as 'production'
|
|
66
68
|
let normalizedEnvironment = config.environment || this.detectEnvironment();
|
|
@@ -68,11 +70,7 @@ export class SafePassage {
|
|
|
68
70
|
console.warn(`SafePassage SDK: Unknown environment '${normalizedEnvironment}', defaulting to 'production'`);
|
|
69
71
|
normalizedEnvironment = 'production';
|
|
70
72
|
}
|
|
71
|
-
this.config = {
|
|
72
|
-
...config,
|
|
73
|
-
environment: normalizedEnvironment,
|
|
74
|
-
mode: config.mode || 'redirect',
|
|
75
|
-
};
|
|
73
|
+
this.config = Object.assign(Object.assign({}, config), { environment: normalizedEnvironment, mode: config.mode || 'redirect' });
|
|
76
74
|
// Comprehensive environment security validation
|
|
77
75
|
validateEnvironmentSecurity(this.config.environment);
|
|
78
76
|
// Enforce HTTPS in production (additional layer)
|
|
@@ -107,6 +105,7 @@ export class SafePassage {
|
|
|
107
105
|
* @throws {Error} If verification cannot be started or is already in progress
|
|
108
106
|
*/
|
|
109
107
|
async verify(options = {}) {
|
|
108
|
+
var _a, _b, _c, _d, _e, _f;
|
|
110
109
|
const isPublicKey = this.isPublicKey();
|
|
111
110
|
// For public keys, create session via API (SafePassage generates the sessionId)
|
|
112
111
|
let sessionId;
|
|
@@ -123,13 +122,13 @@ export class SafePassage {
|
|
|
123
122
|
}
|
|
124
123
|
// Race condition check - prevent multiple simultaneous verifications
|
|
125
124
|
if (this.isVerificationInProgress) {
|
|
126
|
-
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)}...`);
|
|
127
126
|
logSecurityEvent('RACE_CONDITION_PREVENTED', {
|
|
128
|
-
currentSession: this.currentSessionId
|
|
127
|
+
currentSession: ((_b = this.currentSessionId) === null || _b === void 0 ? void 0 : _b.substring(0, 8)) + '...',
|
|
129
128
|
attemptedSession: 'new-session-attempt',
|
|
130
129
|
origin: window.location.origin,
|
|
131
130
|
});
|
|
132
|
-
this.config.onError
|
|
131
|
+
(_d = (_c = this.config).onError) === null || _d === void 0 ? void 0 : _d.call(_c, error);
|
|
133
132
|
throw error;
|
|
134
133
|
}
|
|
135
134
|
// Lock verification process
|
|
@@ -147,7 +146,7 @@ export class SafePassage {
|
|
|
147
146
|
? sessionId.substring(0, 8) + '...'
|
|
148
147
|
: 'undefined',
|
|
149
148
|
});
|
|
150
|
-
this.config.onError
|
|
149
|
+
(_f = (_e = this.config).onError) === null || _f === void 0 ? void 0 : _f.call(_e, error);
|
|
151
150
|
throw error;
|
|
152
151
|
}
|
|
153
152
|
const verificationUrl = await this.buildVerificationUrl(options, sessionId);
|
|
@@ -212,6 +211,9 @@ export class SafePassage {
|
|
|
212
211
|
},
|
|
213
212
|
// Include handoffToken if available (for QR code desktop flow)
|
|
214
213
|
handoffToken: this._temporaryHandoffToken,
|
|
214
|
+
// Include sessionToken and verifyUrl to make UI auth deterministic
|
|
215
|
+
sessionToken: this.lastSessionToken || undefined,
|
|
216
|
+
verifyUrl: this.lastVerifyUrl || undefined,
|
|
215
217
|
}, this.config.environment);
|
|
216
218
|
// Prefer server-provided verifyUrl (contains sessionToken) and append state/mode
|
|
217
219
|
if (this.lastVerifyUrl) {
|
|
@@ -219,13 +221,28 @@ export class SafePassage {
|
|
|
219
221
|
const url = new URL(this.lastVerifyUrl);
|
|
220
222
|
url.searchParams.set('state', state);
|
|
221
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
|
+
}
|
|
222
231
|
return url.toString();
|
|
223
232
|
}
|
|
224
|
-
catch {
|
|
233
|
+
catch (_a) {
|
|
225
234
|
// Fall back to client-constructed URL if parsing fails
|
|
226
235
|
}
|
|
227
236
|
}
|
|
237
|
+
// Client-constructed URL fallback (for backwards compatibility)
|
|
228
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
|
+
}
|
|
229
246
|
return `${baseUrl}/?${params.toString()}`;
|
|
230
247
|
}
|
|
231
248
|
/**
|
|
@@ -252,6 +269,7 @@ export class SafePassage {
|
|
|
252
269
|
* @private
|
|
253
270
|
*/
|
|
254
271
|
openNewTab(url, sessionId) {
|
|
272
|
+
var _a, _b;
|
|
255
273
|
// Clean up any existing resources
|
|
256
274
|
this.cleanup();
|
|
257
275
|
// Clear any existing popup monitor interval
|
|
@@ -262,18 +280,19 @@ export class SafePassage {
|
|
|
262
280
|
// Open new tab
|
|
263
281
|
this.popupWindow = window.open(url, 'safepassage-verify', 'width=600,height=700');
|
|
264
282
|
if (!this.popupWindow) {
|
|
265
|
-
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.'));
|
|
266
284
|
return;
|
|
267
285
|
}
|
|
268
286
|
// Set up PostMessage listener with enhanced security
|
|
269
287
|
this.messageListener = (event) => {
|
|
288
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
270
289
|
// Enhanced origin validation with strict allowlist
|
|
271
290
|
if (!validatePostMessageOrigin(event, this.config.environment)) {
|
|
272
291
|
logSecurityEvent('POSTMESSAGE_ORIGIN_BLOCKED', {
|
|
273
292
|
origin: event.origin,
|
|
274
293
|
environment: this.config.environment,
|
|
275
294
|
expectedOrigins: `SafePassage trusted origins for ${this.config.environment}`,
|
|
276
|
-
messageType: event.data
|
|
295
|
+
messageType: (_a = event.data) === null || _a === void 0 ? void 0 : _a.type,
|
|
277
296
|
});
|
|
278
297
|
return;
|
|
279
298
|
}
|
|
@@ -284,7 +303,7 @@ export class SafePassage {
|
|
|
284
303
|
error: messageValidation.error,
|
|
285
304
|
origin: event.origin,
|
|
286
305
|
sessionId: sessionId.substring(0, 8) + '...',
|
|
287
|
-
messageType: event.data
|
|
306
|
+
messageType: (_b = event.data) === null || _b === void 0 ? void 0 : _b.type,
|
|
288
307
|
});
|
|
289
308
|
return;
|
|
290
309
|
}
|
|
@@ -309,18 +328,19 @@ export class SafePassage {
|
|
|
309
328
|
}
|
|
310
329
|
// Trigger appropriate callback
|
|
311
330
|
if (result.status === 'verified') {
|
|
312
|
-
this.config.onComplete
|
|
331
|
+
(_d = (_c = this.config).onComplete) === null || _d === void 0 ? void 0 : _d.call(_c, result);
|
|
313
332
|
}
|
|
314
333
|
else if (result.status === 'cancelled') {
|
|
315
|
-
this.config.onCancel
|
|
334
|
+
(_f = (_e = this.config).onCancel) === null || _f === void 0 ? void 0 : _f.call(_e);
|
|
316
335
|
}
|
|
317
336
|
else {
|
|
318
|
-
this.config.onError
|
|
337
|
+
(_h = (_g = this.config).onError) === null || _h === void 0 ? void 0 : _h.call(_g, new Error(`Verification failed: ${result.status}`));
|
|
319
338
|
}
|
|
320
339
|
};
|
|
321
340
|
window.addEventListener('message', this.messageListener);
|
|
322
341
|
// Monitor popup window with proper cleanup
|
|
323
342
|
this.popupMonitorInterval = setInterval(() => {
|
|
343
|
+
var _a, _b;
|
|
324
344
|
if (this.popupWindow && this.popupWindow.closed) {
|
|
325
345
|
// Log popup closed event
|
|
326
346
|
logSecurityEvent('POPUP_CLOSED_BY_USER', {
|
|
@@ -331,7 +351,7 @@ export class SafePassage {
|
|
|
331
351
|
this.cleanup();
|
|
332
352
|
// Unlock verification after popup closed
|
|
333
353
|
this.unlockVerification();
|
|
334
|
-
this.config.onCancel
|
|
354
|
+
(_b = (_a = this.config).onCancel) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
335
355
|
}
|
|
336
356
|
}, 500);
|
|
337
357
|
}
|
|
@@ -551,6 +571,7 @@ export class SafePassage {
|
|
|
551
571
|
* @private
|
|
552
572
|
*/
|
|
553
573
|
async createInternalSession(options) {
|
|
574
|
+
var _a, _b;
|
|
554
575
|
try {
|
|
555
576
|
const portalApiUrl = this.getPortalApiUrl();
|
|
556
577
|
const response = await fetch(`${portalApiUrl}/api/v1/sessions/create`, {
|
|
@@ -583,6 +604,10 @@ export class SafePassage {
|
|
|
583
604
|
if (sessionData.verifyUrl) {
|
|
584
605
|
this.lastVerifyUrl = sessionData.verifyUrl;
|
|
585
606
|
}
|
|
607
|
+
// Capture sessionToken for state payload (ensures UI can always auth WS)
|
|
608
|
+
if (sessionData.sessionToken) {
|
|
609
|
+
this.lastSessionToken = sessionData.sessionToken;
|
|
610
|
+
}
|
|
586
611
|
// Store the handoffToken if it exists for desktop QR flow
|
|
587
612
|
if (sessionData.handoffToken) {
|
|
588
613
|
// Store it temporarily so it can be included in the state
|
|
@@ -603,7 +628,7 @@ export class SafePassage {
|
|
|
603
628
|
environment: this.config.environment,
|
|
604
629
|
apiKeyType: 'public',
|
|
605
630
|
});
|
|
606
|
-
this.config.onError
|
|
631
|
+
(_b = (_a = this.config).onError) === null || _b === void 0 ? void 0 : _b.call(_a, error);
|
|
607
632
|
throw new Error(`Failed to create verification session: ${errorMessage}`);
|
|
608
633
|
}
|
|
609
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.3.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 - Updated for
|
|
28
|
-
export const VERSION = '3.0
|
|
27
|
+
// Version - Updated for skip_intro and auto_return support
|
|
28
|
+
export const VERSION = '3.3.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(o=>{if(o.startsWith("*.")){let s=o.slice(2);return i.endsWith(`.${s}`)||i===`https://${s}`||i===`http://${s}`}return i===o})?!0:(console.warn(`SafePassage Security: Blocked PostMessage from untrusted origin: ${i}`,{environment:e,trustedOrigins:x[e],allowedCustomOrigins:n,eventType:t.data?.type}),!1)}function M(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 k(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(o=>n-o<this.timeWindow);return r.length>=this.maxAttempts?(console.warn(`SafePassage Security: Rate limit exceeded for ${e}`),!1):(r.push(n),this.attempts.set(e,r),!0)}reset(e){this.attempts.delete(e)}},L=new 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),o=await crypto.subtle.importKey("raw",i,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),s=await crypto.subtle.sign("HMAC",o,r);return Array.from(new Uint8Array(s)).map(l=>l.toString(16).padStart(2,"0")).join("")}async function _(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),o=await S(i,r);return btoa(JSON.stringify({data:n,signature:o}))}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:o,signature:s}=r,l=JSON.stringify(o),u=y(e);if(!await _(l,s,u))return console.warn("SafePassage: State signature verification failed"),null;if(o.timestamp){let U=Date.now()-o.timestamp;if(U>n)return console.warn("SafePassage: State parameter expired",{age:U,maxAge:n}),null}let{timestamp:ce,nonce:le,...J}=o;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 oe(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),oe(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:()=>se});var c,se,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;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),k(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 s=new Error("Too many verification attempts. Please wait before trying again.");throw a("RATE_LIMIT_EXCEEDED",{apiKey:this.config.apiKey.substring(0,8)+"...",origin:window.location.origin,sessionId:i?i.substring(0,8)+"...":"undefined"}),this.config.onError?.(s),s}let o=await this.buildVerificationUrl(e,i);a("VERIFICATION_INITIATED",{environment:this.config.environment,mode:this.config.mode,sessionId:i?i.substring(0,8)+"...":"undefined",origin:window.location.origin}),this.config.mode==="new-tab"?this.openNewTab(o,i):(this.unlockVerification(),this.redirect(o))}catch(r){throw this.unlockVerification(),r}}async buildVerificationUrl(e,n){let i=I(this.config.environment),r=e.challengeAge!==void 0,o=e.verificationMode!==void 0,s=r||o,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:s,externalUserId:e.externalUserId,timestamp:Date.now(),apiUrl:this.getPortalApiUrl(),engineUrl:this.getEngineUrl(),wsUrl:this.getWebSocketUrl(),environment:this.config.environment,features:{testMode:!1,warmupPeriodMs:500,qualityThreshold:.6},handoffToken:this._temporaryHandoffToken},this.config.environment);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=M(i,n);if(!r.isValid){a("POSTMESSAGE_VALIDATION_FAILED",{error:r.error,origin:i.origin,sessionId:n.substring(0,8)+"...",messageType:i.data?.type});return}let o={sessionId:i.data.sessionId,status:i.data.status};a("VERIFICATION_COMPLETED",{status:o.status,sessionId:n.substring(0,8)+"...",origin:i.origin}),this.cleanup(),this.unlockVerification(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),o.status==="verified"?this.config.onComplete?.(o):o.status==="cancelled"?this.config.onCancel?.():this.config.onError?.(new Error(`Verification failed: ${o.status}`))},window.addEventListener("message",this.messageListener),this.popupMonitorInterval=setInterval(()=>{this.popupWindow&&this.popupWindow.closed&&(a("POPUP_CLOSED_BY_USER",{sessionId:n.substring(0,8)+"...",environment:this.config.environment}),this.cleanup(),this.unlockVerification(),this.config.onCancel?.())},500)}setupAutoCleanup(){if(this.unloadListener=()=>{a("SDK_AUTO_CLEANUP",{environment:this.config.environment,trigger:"page_unload"}),this.cleanup(),this.unlockVerification()},window.addEventListener("beforeunload",this.unloadListener),window.addEventListener("pagehide",this.unloadListener),window.history&&window.history.pushState){let e=window.history.pushState;window.history.pushState=(...n)=>(this.cleanup(),this.unlockVerification(),e.apply(window.history,n))}}detectEnvironment(){let e=window.location.hostname;return e.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 s=await i.json().catch(()=>({}));throw new Error(`Failed to create session: ${i.status} ${i.statusText}. ${s.message||""}`)}let r=await i.json(),o=r.sessionId;if(!o)throw new Error("Server did not return a sessionId");return r.verifyUrl&&(this.lastVerifyUrl=r.verifyUrl),r.handoffToken&&(this._temporaryHandoffToken=r.handoffToken),a("INTERNAL_SESSION_CREATED",{sessionId:o.substring(0,8)+"...",environment:this.config.environment,apiKeyType:"public"}),o}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}`)}}},se=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 q(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,j,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)}},j=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`);if(!t.cancelUrl)throw new Error("cancelUrl is required");if(t.cancelUrl.length>E)throw new Error(`cancelUrl 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}`);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),q(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(!j.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.3.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
|
@@ -68,6 +68,18 @@ export interface VerificationOptions {
|
|
|
68
68
|
* Useful for correlating SafePassage sessions with merchant user records
|
|
69
69
|
*/
|
|
70
70
|
externalUserId?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Skip the intro screen and go directly to camera access
|
|
73
|
+
* Useful for embedded/streamlined flows where user has already consented
|
|
74
|
+
* @default false
|
|
75
|
+
*/
|
|
76
|
+
skipIntro?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Automatically redirect to returnUrl immediately after successful verification
|
|
79
|
+
* Skips the success screen for seamless embedded experiences
|
|
80
|
+
* @default false
|
|
81
|
+
*/
|
|
82
|
+
autoReturn?: boolean;
|
|
71
83
|
}
|
|
72
84
|
export interface VerificationResult {
|
|
73
85
|
/**
|
|
@@ -104,6 +116,8 @@ export interface StatePayload {
|
|
|
104
116
|
qualityThreshold: number;
|
|
105
117
|
};
|
|
106
118
|
handoffToken?: string;
|
|
119
|
+
sessionToken?: string;
|
|
120
|
+
verifyUrl?: string;
|
|
107
121
|
}
|
|
108
122
|
export interface SessionValidationResponse {
|
|
109
123
|
sessionId: string;
|
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
|
}
|