@safepassage/sdk 3.4.2 → 3.4.4
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 +4 -0
- package/dist/core/SafePassageSDK.d.ts +1 -0
- package/dist/core/SafePassageSDK.js +6 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1084 -38
- package/dist/safepassage.min.js +2 -2
- package/dist/types/index.d.ts +4 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -178,6 +178,8 @@ For reliable verification tracking, configure webhooks in your dashboard:
|
|
|
178
178
|
}
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
+
Webhook `timestamp` values are ISO 8601 strings. SDK `onComplete` results use milliseconds since epoch.
|
|
182
|
+
|
|
181
183
|
## Complete Example
|
|
182
184
|
|
|
183
185
|
### HTML + CDN
|
|
@@ -246,6 +248,8 @@ import { SafePassage, SafePassageConfig, VerificationResult } from '@safepassage
|
|
|
246
248
|
const config: SafePassageConfig = {
|
|
247
249
|
apiKey: process.env.NEXT_PUBLIC_SAFEPASSAGE_KEY!,
|
|
248
250
|
returnUrl: '/verified',
|
|
251
|
+
// Optional: used as a fallback redirect for Emblem login failures
|
|
252
|
+
cancelUrl: '/verify-cancelled',
|
|
249
253
|
mode: 'new-tab',
|
|
250
254
|
onComplete: (result: VerificationResult) => {
|
|
251
255
|
console.log(`Session ${result.sessionId}: ${result.status}`);
|
|
@@ -63,6 +63,8 @@ export class SafePassage {
|
|
|
63
63
|
this.lastVerifyUrl = null;
|
|
64
64
|
// Server-provided session token (WS auth)
|
|
65
65
|
this.lastSessionToken = null;
|
|
66
|
+
// Temporary storage for QR handoff token to include in state
|
|
67
|
+
this.temporaryHandoffToken = null;
|
|
66
68
|
validateConfig(config);
|
|
67
69
|
// Normalize environment: treat 'development' or any unknown value as 'production'
|
|
68
70
|
let normalizedEnvironment = config.environment || this.detectEnvironment();
|
|
@@ -210,7 +212,7 @@ export class SafePassage {
|
|
|
210
212
|
qualityThreshold: 0.6,
|
|
211
213
|
},
|
|
212
214
|
// Include handoffToken if available (for QR code desktop flow)
|
|
213
|
-
handoffToken: this.
|
|
215
|
+
handoffToken: this.temporaryHandoffToken || undefined,
|
|
214
216
|
// Include sessionToken and verifyUrl to make UI auth deterministic
|
|
215
217
|
sessionToken: this.lastSessionToken || undefined,
|
|
216
218
|
verifyUrl: this.lastVerifyUrl || undefined,
|
|
@@ -310,6 +312,8 @@ export class SafePassage {
|
|
|
310
312
|
const result = {
|
|
311
313
|
sessionId: event.data.sessionId,
|
|
312
314
|
status: event.data.status,
|
|
315
|
+
timestamp: event.data.timestamp,
|
|
316
|
+
externalUserId: event.data.externalUserId,
|
|
313
317
|
};
|
|
314
318
|
// Log successful verification completion
|
|
315
319
|
logSecurityEvent('VERIFICATION_COMPLETED', {
|
|
@@ -609,7 +613,7 @@ export class SafePassage {
|
|
|
609
613
|
// Store the handoffToken if it exists for desktop QR flow
|
|
610
614
|
if (sessionData.handoffToken) {
|
|
611
615
|
// Store it temporarily so it can be included in the state
|
|
612
|
-
this.
|
|
616
|
+
this.temporaryHandoffToken = sessionData.handoffToken;
|
|
613
617
|
}
|
|
614
618
|
// Log successful session creation
|
|
615
619
|
logSecurityEvent('INTERNAL_SESSION_CREATED', {
|
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.4.
|
|
21
|
+
export declare const VERSION = "3.4.4";
|
package/dist/index.js
CHANGED
|
@@ -1,40 +1,1086 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*/
|
|
19
|
-
// Setup polyfills and check browser compatibility
|
|
20
|
-
import { setupPolyfills, checkBrowserCompatibility } from './utils/polyfills';
|
|
21
|
-
// Initialize polyfills immediately
|
|
22
|
-
if (typeof window !== 'undefined') {
|
|
23
|
-
setupPolyfills();
|
|
24
|
-
checkBrowserCompatibility();
|
|
25
|
-
}
|
|
26
|
-
export { SafePassage, SafePassage as default } from './core/SafePassageSDK';
|
|
27
|
-
// Version - 3.4.2: Fixed CDN references, aligned documentation
|
|
28
|
-
export const VERSION = '3.4.2';
|
|
29
|
-
// For UMD builds
|
|
30
|
-
if (typeof window !== 'undefined' && window) {
|
|
31
|
-
// Dynamic import for UMD builds
|
|
32
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
33
|
-
const { SafePassage } = require('./core/SafePassageSDK');
|
|
34
|
-
// Attach to window object for global access
|
|
35
|
-
const globalWindow = window;
|
|
36
|
-
globalWindow.SafePassage = SafePassage;
|
|
37
|
-
if (globalWindow.SafePassage) {
|
|
38
|
-
globalWindow.SafePassage.VERSION = VERSION;
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defProps = Object.defineProperties;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
9
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
10
|
+
var __spreadValues = (a, b) => {
|
|
11
|
+
for (var prop in b || (b = {}))
|
|
12
|
+
if (__hasOwnProp.call(b, prop))
|
|
13
|
+
__defNormalProp(a, prop, b[prop]);
|
|
14
|
+
if (__getOwnPropSymbols)
|
|
15
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
16
|
+
if (__propIsEnum.call(b, prop))
|
|
17
|
+
__defNormalProp(a, prop, b[prop]);
|
|
39
18
|
}
|
|
19
|
+
return a;
|
|
20
|
+
};
|
|
21
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
22
|
+
var __objRest = (source, exclude) => {
|
|
23
|
+
var target = {};
|
|
24
|
+
for (var prop in source)
|
|
25
|
+
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
26
|
+
target[prop] = source[prop];
|
|
27
|
+
if (source != null && __getOwnPropSymbols)
|
|
28
|
+
for (var prop of __getOwnPropSymbols(source)) {
|
|
29
|
+
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
30
|
+
target[prop] = source[prop];
|
|
31
|
+
}
|
|
32
|
+
return target;
|
|
33
|
+
};
|
|
34
|
+
var __esm = (fn, res) => function __init() {
|
|
35
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
36
|
+
};
|
|
37
|
+
var __export = (target, all) => {
|
|
38
|
+
for (var name in all)
|
|
39
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
40
|
+
};
|
|
41
|
+
var __copyProps = (to, from, except, desc) => {
|
|
42
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
43
|
+
for (let key of __getOwnPropNames(from))
|
|
44
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
45
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
46
|
+
}
|
|
47
|
+
return to;
|
|
48
|
+
};
|
|
49
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
50
|
+
|
|
51
|
+
// src-redirect/utils/security.ts
|
|
52
|
+
function isOriginTrusted(origin, environment) {
|
|
53
|
+
const trustedOrigins = TRUSTED_ORIGINS[environment];
|
|
54
|
+
return trustedOrigins.includes(origin);
|
|
55
|
+
}
|
|
56
|
+
function validatePostMessageOrigin(event, environment, allowedCustomOrigins = []) {
|
|
57
|
+
var _a;
|
|
58
|
+
const { origin } = event;
|
|
59
|
+
if (isOriginTrusted(origin, environment)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (allowedCustomOrigins.length > 0) {
|
|
63
|
+
const isCustomOriginAllowed = allowedCustomOrigins.some((allowedOrigin) => {
|
|
64
|
+
if (allowedOrigin.startsWith("*.")) {
|
|
65
|
+
const domain = allowedOrigin.slice(2);
|
|
66
|
+
return origin.endsWith(`.${domain}`) || origin === `https://${domain}` || origin === `http://${domain}`;
|
|
67
|
+
}
|
|
68
|
+
return origin === allowedOrigin;
|
|
69
|
+
});
|
|
70
|
+
if (isCustomOriginAllowed) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
console.warn(
|
|
75
|
+
`SafePassage Security: Blocked PostMessage from untrusted origin: ${origin}`,
|
|
76
|
+
{
|
|
77
|
+
environment,
|
|
78
|
+
trustedOrigins: TRUSTED_ORIGINS[environment],
|
|
79
|
+
allowedCustomOrigins,
|
|
80
|
+
eventType: (_a = event.data) == null ? void 0 : _a.type
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
function validateSafePassageMessage(event, expectedSessionId) {
|
|
86
|
+
const { data } = event;
|
|
87
|
+
if (!data || typeof data !== "object") {
|
|
88
|
+
return { isValid: false, error: "Invalid message format" };
|
|
89
|
+
}
|
|
90
|
+
if (data.type !== "safepassage:verification:complete") {
|
|
91
|
+
return { isValid: false, error: "Invalid message type" };
|
|
92
|
+
}
|
|
93
|
+
if (!data.sessionId || data.sessionId !== expectedSessionId) {
|
|
94
|
+
return { isValid: false, error: "Session ID mismatch" };
|
|
95
|
+
}
|
|
96
|
+
if (!data.status || !["verified", "failed"].includes(data.status)) {
|
|
97
|
+
return { isValid: false, error: "Invalid status value" };
|
|
98
|
+
}
|
|
99
|
+
return { isValid: true };
|
|
100
|
+
}
|
|
101
|
+
function enforceHTTPS(environment) {
|
|
102
|
+
if (environment === "production" && window.location.protocol !== "https:") {
|
|
103
|
+
console.warn(
|
|
104
|
+
"SafePassage Warning: HTTPS recommended for production environment",
|
|
105
|
+
{
|
|
106
|
+
current: window.location.href
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function validateReturnUrl(url, environment) {
|
|
112
|
+
try {
|
|
113
|
+
const parsed = new URL(url);
|
|
114
|
+
if (parsed.protocol !== "https:") {
|
|
115
|
+
const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
116
|
+
if (!isLocalhost) {
|
|
117
|
+
return {
|
|
118
|
+
isValid: false,
|
|
119
|
+
error: `HTTPS required for return URLs in ${environment}`
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const suspiciousPatterns = [
|
|
124
|
+
/data:/i,
|
|
125
|
+
/javascript:/i,
|
|
126
|
+
/vbscript:/i,
|
|
127
|
+
/file:/i,
|
|
128
|
+
/ftp:/i
|
|
129
|
+
];
|
|
130
|
+
for (const pattern of suspiciousPatterns) {
|
|
131
|
+
if (pattern.test(url)) {
|
|
132
|
+
return { isValid: false, error: "Blocked suspicious URL scheme" };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { isValid: true };
|
|
136
|
+
} catch (e) {
|
|
137
|
+
return { isValid: false, error: "Invalid URL format" };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function logSecurityEvent(event, details) {
|
|
141
|
+
console.warn(`SafePassage Security Event: ${event}`, __spreadValues({
|
|
142
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
143
|
+
userAgent: navigator.userAgent,
|
|
144
|
+
url: window.location.href
|
|
145
|
+
}, details));
|
|
146
|
+
}
|
|
147
|
+
var TRUSTED_ORIGINS, VerificationRateLimit, verificationRateLimit;
|
|
148
|
+
var init_security = __esm({
|
|
149
|
+
"src-redirect/utils/security.ts"() {
|
|
150
|
+
"use strict";
|
|
151
|
+
TRUSTED_ORIGINS = {
|
|
152
|
+
production: [
|
|
153
|
+
"https://av.safepassageapp.com",
|
|
154
|
+
"https://portal.safepassageapp.com",
|
|
155
|
+
"https://api.safepassageapp.com"
|
|
156
|
+
],
|
|
157
|
+
staging: [
|
|
158
|
+
"https://av.staging.safepassageapp.com",
|
|
159
|
+
"https://portal.staging.safepassageapp.com",
|
|
160
|
+
"https://api.staging.safepassageapp.com"
|
|
161
|
+
]
|
|
162
|
+
};
|
|
163
|
+
VerificationRateLimit = class {
|
|
164
|
+
constructor() {
|
|
165
|
+
this.attempts = /* @__PURE__ */ new Map();
|
|
166
|
+
this.maxAttempts = 5;
|
|
167
|
+
this.timeWindow = 6e4;
|
|
168
|
+
}
|
|
169
|
+
// 1 minute
|
|
170
|
+
isAllowed(identifier) {
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
const attempts = this.attempts.get(identifier) || [];
|
|
173
|
+
const recentAttempts = attempts.filter(
|
|
174
|
+
(time) => now - time < this.timeWindow
|
|
175
|
+
);
|
|
176
|
+
if (recentAttempts.length >= this.maxAttempts) {
|
|
177
|
+
console.warn(
|
|
178
|
+
`SafePassage Security: Rate limit exceeded for ${identifier}`
|
|
179
|
+
);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
recentAttempts.push(now);
|
|
183
|
+
this.attempts.set(identifier, recentAttempts);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
reset(identifier) {
|
|
187
|
+
this.attempts.delete(identifier);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
verificationRateLimit = new VerificationRateLimit();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// src-redirect/utils/crypto.ts
|
|
195
|
+
var crypto_exports = {};
|
|
196
|
+
__export(crypto_exports, {
|
|
197
|
+
createSignedState: () => createSignedState,
|
|
198
|
+
generateHMAC: () => generateHMAC,
|
|
199
|
+
generateSecureToken: () => generateSecureToken,
|
|
200
|
+
getSigningSecret: () => getSigningSecret,
|
|
201
|
+
parseSignedState: () => parseSignedState,
|
|
202
|
+
verifyHMAC: () => verifyHMAC
|
|
203
|
+
});
|
|
204
|
+
async function generateHMAC(data, secret) {
|
|
205
|
+
const encoder = new TextEncoder();
|
|
206
|
+
const keyData = encoder.encode(secret);
|
|
207
|
+
const dataBuffer = encoder.encode(data);
|
|
208
|
+
const key = await crypto.subtle.importKey(
|
|
209
|
+
"raw",
|
|
210
|
+
keyData,
|
|
211
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
212
|
+
false,
|
|
213
|
+
["sign"]
|
|
214
|
+
);
|
|
215
|
+
const signature = await crypto.subtle.sign("HMAC", key, dataBuffer);
|
|
216
|
+
return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
217
|
+
}
|
|
218
|
+
async function verifyHMAC(data, signature, secret) {
|
|
219
|
+
try {
|
|
220
|
+
const expectedSignature = await generateHMAC(data, secret);
|
|
221
|
+
return constantTimeCompare(signature, expectedSignature);
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function constantTimeCompare(a, b) {
|
|
227
|
+
if (a.length !== b.length) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
let result = 0;
|
|
231
|
+
for (let i = 0; i < a.length; i++) {
|
|
232
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
233
|
+
}
|
|
234
|
+
return result === 0;
|
|
235
|
+
}
|
|
236
|
+
function generateSecureToken(length = 32) {
|
|
237
|
+
const array = new Uint8Array(length);
|
|
238
|
+
crypto.getRandomValues(array);
|
|
239
|
+
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
240
|
+
}
|
|
241
|
+
function getSigningSecret(environment) {
|
|
242
|
+
const baseSecrets = {
|
|
243
|
+
production: "safepassage-prod-hmac-2025",
|
|
244
|
+
staging: "safepassage-stage-hmac-2025"
|
|
245
|
+
};
|
|
246
|
+
return baseSecrets[environment];
|
|
247
|
+
}
|
|
248
|
+
async function createSignedState(payload, environment) {
|
|
249
|
+
const timestampedPayload = __spreadProps(__spreadValues({}, payload), {
|
|
250
|
+
timestamp: Date.now(),
|
|
251
|
+
nonce: generateSecureToken(16)
|
|
252
|
+
// Add nonce to prevent replay attacks
|
|
253
|
+
});
|
|
254
|
+
const dataString = JSON.stringify(timestampedPayload);
|
|
255
|
+
const secret = getSigningSecret(environment);
|
|
256
|
+
const signature = await generateHMAC(dataString, secret);
|
|
257
|
+
const signedPayload = {
|
|
258
|
+
data: timestampedPayload,
|
|
259
|
+
signature
|
|
260
|
+
};
|
|
261
|
+
return btoa(JSON.stringify(signedPayload));
|
|
262
|
+
}
|
|
263
|
+
async function parseSignedState(signedState, environment, maxAge = STATE_EXPIRY_MS) {
|
|
264
|
+
try {
|
|
265
|
+
const json = atob(signedState);
|
|
266
|
+
const signedPayload = JSON.parse(json);
|
|
267
|
+
if (!signedPayload.data || !signedPayload.signature) {
|
|
268
|
+
console.warn("SafePassage: Invalid signed state format");
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const { data, signature } = signedPayload;
|
|
272
|
+
const dataString = JSON.stringify(data);
|
|
273
|
+
const secret = getSigningSecret(environment);
|
|
274
|
+
const isValid = await verifyHMAC(dataString, signature, secret);
|
|
275
|
+
if (!isValid) {
|
|
276
|
+
console.warn("SafePassage: State signature verification failed");
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
if (data.timestamp) {
|
|
280
|
+
const age = Date.now() - data.timestamp;
|
|
281
|
+
if (age > maxAge) {
|
|
282
|
+
console.warn("SafePassage: State parameter expired", { age, maxAge });
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const _a = data, { timestamp, nonce } = _a, payload = __objRest(_a, ["timestamp", "nonce"]);
|
|
287
|
+
return payload;
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.warn("SafePassage: Failed to parse signed state", error);
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
var init_crypto = __esm({
|
|
294
|
+
"src-redirect/utils/crypto.ts"() {
|
|
295
|
+
"use strict";
|
|
296
|
+
init_validation();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// src-redirect/utils/validation.ts
|
|
301
|
+
function validateConfig(config) {
|
|
302
|
+
if (!config.apiKey) {
|
|
303
|
+
throw new Error("apiKey is required");
|
|
304
|
+
}
|
|
305
|
+
if (config.apiKey.length > MAX_API_KEY_LENGTH) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`apiKey exceeds maximum length of ${MAX_API_KEY_LENGTH} characters`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (!API_KEY_PATTERN.test(config.apiKey)) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
"Invalid apiKey format. Expected pk_xxx (public) or sk_xxx (private)"
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
if (typeof window !== "undefined" && config.apiKey.startsWith("sk_")) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
"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"
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
if (!config.returnUrl) {
|
|
321
|
+
throw new Error("returnUrl is required");
|
|
322
|
+
}
|
|
323
|
+
if (config.returnUrl.length > MAX_URL_LENGTH) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`returnUrl exceeds maximum length of ${MAX_URL_LENGTH} characters`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const environment = detectEnvironment();
|
|
329
|
+
const returnUrlValidation = validateReturnUrl(config.returnUrl, environment);
|
|
330
|
+
if (!returnUrlValidation.isValid) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`returnUrl validation failed: ${returnUrlValidation.error}`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
if (config.cancelUrl) {
|
|
336
|
+
if (config.cancelUrl.length > MAX_URL_LENGTH) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`cancelUrl exceeds maximum length of ${MAX_URL_LENGTH} characters`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
const cancelUrlValidation = validateReturnUrl(config.cancelUrl, environment);
|
|
342
|
+
if (!cancelUrlValidation.isValid) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`cancelUrl validation failed: ${cancelUrlValidation.error}`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (config.defaultChallengeAge !== void 0) {
|
|
349
|
+
if (config.defaultChallengeAge < MINIMUM_AGE) {
|
|
350
|
+
throw new Error(`defaultChallengeAge must be at least ${MINIMUM_AGE}`);
|
|
351
|
+
}
|
|
352
|
+
if (config.defaultChallengeAge > MAXIMUM_AGE) {
|
|
353
|
+
throw new Error(`defaultChallengeAge cannot exceed ${MAXIMUM_AGE}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (config.defaultVerificationMode && !["L1", "L2"].includes(config.defaultVerificationMode)) {
|
|
357
|
+
throw new Error("defaultVerificationMode must be L1 or L2");
|
|
358
|
+
}
|
|
359
|
+
if (config.mode && !["redirect", "new-tab"].includes(config.mode)) {
|
|
360
|
+
throw new Error("mode must be redirect or new-tab");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function detectEnvironment() {
|
|
364
|
+
if (typeof window === "undefined") {
|
|
365
|
+
return "production";
|
|
366
|
+
}
|
|
367
|
+
const hostname = window.location.hostname;
|
|
368
|
+
if (hostname.includes("staging") || hostname.includes("stage")) {
|
|
369
|
+
return "staging";
|
|
370
|
+
}
|
|
371
|
+
return "production";
|
|
372
|
+
}
|
|
373
|
+
async function generateState(payload, environment) {
|
|
374
|
+
const { createSignedState: createSignedState2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
375
|
+
return createSignedState2(payload, environment);
|
|
376
|
+
}
|
|
377
|
+
var MINIMUM_AGE, MAXIMUM_AGE, MAX_URL_LENGTH, MAX_API_KEY_LENGTH, STATE_EXPIRY_MS, API_KEY_PATTERN;
|
|
378
|
+
var init_validation = __esm({
|
|
379
|
+
"src-redirect/utils/validation.ts"() {
|
|
380
|
+
"use strict";
|
|
381
|
+
init_security();
|
|
382
|
+
MINIMUM_AGE = 25;
|
|
383
|
+
MAXIMUM_AGE = 150;
|
|
384
|
+
MAX_URL_LENGTH = 2048;
|
|
385
|
+
MAX_API_KEY_LENGTH = 128;
|
|
386
|
+
STATE_EXPIRY_MS = 6e5;
|
|
387
|
+
API_KEY_PATTERN = /^(pk_|sk_)[a-zA-Z0-9_]+$/;
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// src-redirect/utils/environment.ts
|
|
392
|
+
function getEnvironmentUrl(environment) {
|
|
393
|
+
const url = ENVIRONMENT_URLS[environment] || ENVIRONMENT_URLS.production;
|
|
394
|
+
if (!url || !url.startsWith("https://")) {
|
|
395
|
+
throw new Error(`HTTPS required for ${environment} environment`);
|
|
396
|
+
}
|
|
397
|
+
return url;
|
|
398
|
+
}
|
|
399
|
+
function getApiUrl(environment) {
|
|
400
|
+
const apiUrls = {
|
|
401
|
+
production: "https://api.safepassageapp.com",
|
|
402
|
+
staging: "https://api.staging.safepassageapp.com"
|
|
403
|
+
};
|
|
404
|
+
const url = apiUrls[environment] || apiUrls.production;
|
|
405
|
+
if (!url || !url.startsWith("https://")) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`HTTPS required for API URLs in ${environment} environment`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return url;
|
|
411
|
+
}
|
|
412
|
+
function validateEnvironmentSecurity(environment) {
|
|
413
|
+
const isSecure = window.location.protocol === "https:";
|
|
414
|
+
switch (environment) {
|
|
415
|
+
case "production":
|
|
416
|
+
if (!isSecure) {
|
|
417
|
+
console.warn("SafePassage Warning: HTTPS recommended for production environment");
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
case "staging":
|
|
421
|
+
if (!isSecure) {
|
|
422
|
+
console.warn(
|
|
423
|
+
"SafePassage Warning: HTTPS strongly recommended in staging environment"
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
getEnvironmentUrl(environment);
|
|
430
|
+
getApiUrl(environment);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
433
|
+
throw new Error(`Environment configuration validation failed: ${errorMessage}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
var ENVIRONMENT_URLS;
|
|
437
|
+
var init_environment = __esm({
|
|
438
|
+
"src-redirect/utils/environment.ts"() {
|
|
439
|
+
"use strict";
|
|
440
|
+
ENVIRONMENT_URLS = {
|
|
441
|
+
production: "https://av.safepassageapp.com",
|
|
442
|
+
staging: "https://av.staging.safepassageapp.com"
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// src-redirect/core/SafePassageSDK.ts
|
|
448
|
+
var SafePassageSDK_exports = {};
|
|
449
|
+
__export(SafePassageSDK_exports, {
|
|
450
|
+
SafePassage: () => SafePassage,
|
|
451
|
+
default: () => SafePassageSDK_default
|
|
452
|
+
});
|
|
453
|
+
var SafePassage, SafePassageSDK_default;
|
|
454
|
+
var init_SafePassageSDK = __esm({
|
|
455
|
+
"src-redirect/core/SafePassageSDK.ts"() {
|
|
456
|
+
"use strict";
|
|
457
|
+
init_validation();
|
|
458
|
+
init_environment();
|
|
459
|
+
init_security();
|
|
460
|
+
SafePassage = class {
|
|
461
|
+
/**
|
|
462
|
+
* Initialize SafePassage SDK
|
|
463
|
+
*
|
|
464
|
+
* Validates configuration, sets up security measures, and prepares the SDK
|
|
465
|
+
* for verification operations. Performs comprehensive environment validation
|
|
466
|
+
* and security initialization.
|
|
467
|
+
*
|
|
468
|
+
* @param {SafePassageConfig} config - SDK configuration object
|
|
469
|
+
* @throws {Error} If configuration validation fails
|
|
470
|
+
*/
|
|
471
|
+
constructor(config) {
|
|
472
|
+
this.popupWindow = null;
|
|
473
|
+
this.messageListener = null;
|
|
474
|
+
this.popupMonitorInterval = null;
|
|
475
|
+
this.unloadListener = null;
|
|
476
|
+
this.isVerificationInProgress = false;
|
|
477
|
+
this.currentSessionId = null;
|
|
478
|
+
// Server-provided verify URL (includes sessionToken)
|
|
479
|
+
this.lastVerifyUrl = null;
|
|
480
|
+
// Server-provided session token (WS auth)
|
|
481
|
+
this.lastSessionToken = null;
|
|
482
|
+
// Temporary storage for QR handoff token to include in state
|
|
483
|
+
this.temporaryHandoffToken = null;
|
|
484
|
+
validateConfig(config);
|
|
485
|
+
let normalizedEnvironment = config.environment || this.detectEnvironment();
|
|
486
|
+
if (normalizedEnvironment !== "staging" && normalizedEnvironment !== "production") {
|
|
487
|
+
console.warn(`SafePassage SDK: Unknown environment '${normalizedEnvironment}', defaulting to 'production'`);
|
|
488
|
+
normalizedEnvironment = "production";
|
|
489
|
+
}
|
|
490
|
+
this.config = __spreadProps(__spreadValues({}, config), {
|
|
491
|
+
environment: normalizedEnvironment,
|
|
492
|
+
mode: config.mode || "redirect"
|
|
493
|
+
});
|
|
494
|
+
validateEnvironmentSecurity(this.config.environment);
|
|
495
|
+
enforceHTTPS(this.config.environment);
|
|
496
|
+
logSecurityEvent("SDK_INITIALIZED", {
|
|
497
|
+
environment: this.config.environment,
|
|
498
|
+
mode: this.config.mode,
|
|
499
|
+
origin: window.location.origin,
|
|
500
|
+
protocol: window.location.protocol,
|
|
501
|
+
hostname: window.location.hostname
|
|
502
|
+
});
|
|
503
|
+
this.setupAutoCleanup();
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Initiate age verification with race condition protection
|
|
507
|
+
*
|
|
508
|
+
* Main verification method that handles session creation, security validation,
|
|
509
|
+
* and verification flow initiation. Includes race condition protection and
|
|
510
|
+
* comprehensive error handling.
|
|
511
|
+
*
|
|
512
|
+
* For public keys (pk_*), automatically creates sessions via the portal API.
|
|
513
|
+
* For private keys (sk_*), requires a pre-created sessionId.
|
|
514
|
+
*
|
|
515
|
+
* @param {VerificationOptions} [options={}] - Verification options
|
|
516
|
+
* @param {string} [options.sessionId] - Session ID (required for private keys)
|
|
517
|
+
* @param {number} [options.challengeAge] - Age challenge override
|
|
518
|
+
* @param {string} [options.verificationMode] - Verification mode override
|
|
519
|
+
* @param {string} [options.externalUserId] - External user identifier
|
|
520
|
+
* @returns {Promise<void>} Promise that resolves when verification is initiated
|
|
521
|
+
* @throws {Error} If verification cannot be started or is already in progress
|
|
522
|
+
*/
|
|
523
|
+
async verify(options = {}) {
|
|
524
|
+
var _a, _b, _c, _d, _e, _f;
|
|
525
|
+
const isPublicKey = this.isPublicKey();
|
|
526
|
+
let sessionId;
|
|
527
|
+
if (isPublicKey) {
|
|
528
|
+
sessionId = await this.createInternalSession(options);
|
|
529
|
+
} else {
|
|
530
|
+
throw new Error(
|
|
531
|
+
"Private API keys (sk_) should use the direct API, not the SDK. The SDK is designed for browser-based public key usage only."
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
if (!sessionId) {
|
|
535
|
+
throw new Error("Failed to obtain sessionId from server");
|
|
536
|
+
}
|
|
537
|
+
if (this.isVerificationInProgress) {
|
|
538
|
+
const error = new Error(
|
|
539
|
+
`Verification already in progress for session ${(_a = this.currentSessionId) == null ? void 0 : _a.substring(0, 8)}...`
|
|
540
|
+
);
|
|
541
|
+
logSecurityEvent("RACE_CONDITION_PREVENTED", {
|
|
542
|
+
currentSession: ((_b = this.currentSessionId) == null ? void 0 : _b.substring(0, 8)) + "...",
|
|
543
|
+
attemptedSession: "new-session-attempt",
|
|
544
|
+
origin: window.location.origin
|
|
545
|
+
});
|
|
546
|
+
(_d = (_c = this.config).onError) == null ? void 0 : _d.call(_c, error);
|
|
547
|
+
throw error;
|
|
548
|
+
}
|
|
549
|
+
this.isVerificationInProgress = true;
|
|
550
|
+
this.currentSessionId = sessionId;
|
|
551
|
+
try {
|
|
552
|
+
const rateLimitKey = `${this.config.apiKey}:${window.location.origin}`;
|
|
553
|
+
if (!verificationRateLimit.isAllowed(rateLimitKey)) {
|
|
554
|
+
const error = new Error(
|
|
555
|
+
"Too many verification attempts. Please wait before trying again."
|
|
556
|
+
);
|
|
557
|
+
logSecurityEvent("RATE_LIMIT_EXCEEDED", {
|
|
558
|
+
apiKey: this.config.apiKey.substring(0, 8) + "...",
|
|
559
|
+
origin: window.location.origin,
|
|
560
|
+
sessionId: sessionId ? sessionId.substring(0, 8) + "..." : "undefined"
|
|
561
|
+
});
|
|
562
|
+
(_f = (_e = this.config).onError) == null ? void 0 : _f.call(_e, error);
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
565
|
+
const verificationUrl = await this.buildVerificationUrl(
|
|
566
|
+
options,
|
|
567
|
+
sessionId
|
|
568
|
+
);
|
|
569
|
+
logSecurityEvent("VERIFICATION_INITIATED", {
|
|
570
|
+
environment: this.config.environment,
|
|
571
|
+
mode: this.config.mode,
|
|
572
|
+
sessionId: sessionId ? sessionId.substring(0, 8) + "..." : "undefined",
|
|
573
|
+
origin: window.location.origin
|
|
574
|
+
});
|
|
575
|
+
if (this.config.mode === "new-tab") {
|
|
576
|
+
this.openNewTab(verificationUrl, sessionId);
|
|
577
|
+
} else {
|
|
578
|
+
this.unlockVerification();
|
|
579
|
+
this.redirect(verificationUrl);
|
|
580
|
+
}
|
|
581
|
+
} catch (error) {
|
|
582
|
+
this.unlockVerification();
|
|
583
|
+
throw error;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Build verification URL with HMAC-signed state
|
|
588
|
+
*
|
|
589
|
+
* Constructs the verification URL with signed state parameter containing
|
|
590
|
+
* all necessary configuration and security information. The state parameter
|
|
591
|
+
* includes HMAC signature for integrity protection.
|
|
592
|
+
*
|
|
593
|
+
* @param {VerificationOptions} options - Verification options
|
|
594
|
+
* @returns {Promise<string>} Complete verification URL
|
|
595
|
+
* @private
|
|
596
|
+
*/
|
|
597
|
+
async buildVerificationUrl(options, sessionId) {
|
|
598
|
+
const baseUrl = getEnvironmentUrl(this.config.environment);
|
|
599
|
+
const hasExplicitChallengeAge = options.challengeAge !== void 0;
|
|
600
|
+
const hasExplicitVerificationMode = options.verificationMode !== void 0;
|
|
601
|
+
const hasOverrides = hasExplicitChallengeAge || hasExplicitVerificationMode;
|
|
602
|
+
const state = await generateState(
|
|
603
|
+
{
|
|
604
|
+
merchantId: this.config.apiKey,
|
|
605
|
+
sessionId,
|
|
606
|
+
returnUrl: this.config.returnUrl,
|
|
607
|
+
cancelUrl: this.config.cancelUrl,
|
|
608
|
+
challengeAge: options.challengeAge || this.config.defaultChallengeAge,
|
|
609
|
+
verificationMode: options.verificationMode || this.config.defaultVerificationMode,
|
|
610
|
+
hasOverrides,
|
|
611
|
+
// Flag to indicate explicit overrides
|
|
612
|
+
externalUserId: options.externalUserId,
|
|
613
|
+
timestamp: Date.now(),
|
|
614
|
+
// Phase 2: Include config for self-contained verify-ui
|
|
615
|
+
apiUrl: this.getPortalApiUrl(),
|
|
616
|
+
engineUrl: this.getEngineUrl(),
|
|
617
|
+
wsUrl: this.getWebSocketUrl(),
|
|
618
|
+
environment: this.config.environment,
|
|
619
|
+
features: {
|
|
620
|
+
testMode: false,
|
|
621
|
+
warmupPeriodMs: 500,
|
|
622
|
+
qualityThreshold: 0.6
|
|
623
|
+
},
|
|
624
|
+
// Include handoffToken if available (for QR code desktop flow)
|
|
625
|
+
handoffToken: this.temporaryHandoffToken || void 0,
|
|
626
|
+
// Include sessionToken and verifyUrl to make UI auth deterministic
|
|
627
|
+
sessionToken: this.lastSessionToken || void 0,
|
|
628
|
+
verifyUrl: this.lastVerifyUrl || void 0
|
|
629
|
+
},
|
|
630
|
+
this.config.environment
|
|
631
|
+
);
|
|
632
|
+
if (this.lastVerifyUrl) {
|
|
633
|
+
try {
|
|
634
|
+
const url = new URL(this.lastVerifyUrl);
|
|
635
|
+
url.searchParams.set("state", state);
|
|
636
|
+
url.searchParams.set("mode", this.config.mode);
|
|
637
|
+
if (options.skipIntro) {
|
|
638
|
+
url.searchParams.set("skip_intro", "true");
|
|
639
|
+
}
|
|
640
|
+
if (options.autoReturn) {
|
|
641
|
+
url.searchParams.set("auto_return", "true");
|
|
642
|
+
}
|
|
643
|
+
return url.toString();
|
|
644
|
+
} catch (e) {
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
const params = new URLSearchParams({ state, sessionId, mode: this.config.mode });
|
|
648
|
+
if (options.skipIntro) {
|
|
649
|
+
params.set("skip_intro", "true");
|
|
650
|
+
}
|
|
651
|
+
if (options.autoReturn) {
|
|
652
|
+
params.set("auto_return", "true");
|
|
653
|
+
}
|
|
654
|
+
return `${baseUrl}/?${params.toString()}`;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Redirect in same tab
|
|
658
|
+
*
|
|
659
|
+
* Performs a full page redirect to the verification URL.
|
|
660
|
+
* Used for redirect mode verification.
|
|
661
|
+
*
|
|
662
|
+
* @param {string} url - Verification URL to redirect to
|
|
663
|
+
* @private
|
|
664
|
+
*/
|
|
665
|
+
redirect(url) {
|
|
666
|
+
window.location.href = url;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Open in new tab with PostMessage communication and proper cleanup
|
|
670
|
+
*
|
|
671
|
+
* Opens verification URL in a new tab/window and sets up secure PostMessage
|
|
672
|
+
* communication for receiving verification results. Includes comprehensive
|
|
673
|
+
* security validation and automatic cleanup.
|
|
674
|
+
*
|
|
675
|
+
* @param {string} url - Verification URL to open
|
|
676
|
+
* @param {string} sessionId - Session ID for result correlation
|
|
677
|
+
* @private
|
|
678
|
+
*/
|
|
679
|
+
openNewTab(url, sessionId) {
|
|
680
|
+
var _a, _b;
|
|
681
|
+
this.cleanup();
|
|
682
|
+
if (this.popupMonitorInterval) {
|
|
683
|
+
clearInterval(this.popupMonitorInterval);
|
|
684
|
+
this.popupMonitorInterval = null;
|
|
685
|
+
}
|
|
686
|
+
this.popupWindow = window.open(
|
|
687
|
+
url,
|
|
688
|
+
"safepassage-verify",
|
|
689
|
+
"width=600,height=700"
|
|
690
|
+
);
|
|
691
|
+
if (!this.popupWindow) {
|
|
692
|
+
(_b = (_a = this.config).onError) == null ? void 0 : _b.call(
|
|
693
|
+
_a,
|
|
694
|
+
new Error(
|
|
695
|
+
"Failed to open verification window. Please check popup blocker settings."
|
|
696
|
+
)
|
|
697
|
+
);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
this.messageListener = (event) => {
|
|
701
|
+
var _a2, _b2, _c, _d, _e, _f;
|
|
702
|
+
if (!validatePostMessageOrigin(event, this.config.environment)) {
|
|
703
|
+
logSecurityEvent("POSTMESSAGE_ORIGIN_BLOCKED", {
|
|
704
|
+
origin: event.origin,
|
|
705
|
+
environment: this.config.environment,
|
|
706
|
+
expectedOrigins: `SafePassage trusted origins for ${this.config.environment}`,
|
|
707
|
+
messageType: (_a2 = event.data) == null ? void 0 : _a2.type
|
|
708
|
+
});
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const messageValidation = validateSafePassageMessage(event, sessionId);
|
|
712
|
+
if (!messageValidation.isValid) {
|
|
713
|
+
logSecurityEvent("POSTMESSAGE_VALIDATION_FAILED", {
|
|
714
|
+
error: messageValidation.error,
|
|
715
|
+
origin: event.origin,
|
|
716
|
+
sessionId: sessionId.substring(0, 8) + "...",
|
|
717
|
+
messageType: (_b2 = event.data) == null ? void 0 : _b2.type
|
|
718
|
+
});
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const result = {
|
|
722
|
+
sessionId: event.data.sessionId,
|
|
723
|
+
status: event.data.status,
|
|
724
|
+
timestamp: event.data.timestamp,
|
|
725
|
+
externalUserId: event.data.externalUserId
|
|
726
|
+
};
|
|
727
|
+
logSecurityEvent("VERIFICATION_COMPLETED", {
|
|
728
|
+
status: result.status,
|
|
729
|
+
sessionId: sessionId.substring(0, 8) + "...",
|
|
730
|
+
origin: event.origin
|
|
731
|
+
});
|
|
732
|
+
this.cleanup();
|
|
733
|
+
this.unlockVerification();
|
|
734
|
+
if (this.popupMonitorInterval) {
|
|
735
|
+
clearInterval(this.popupMonitorInterval);
|
|
736
|
+
this.popupMonitorInterval = null;
|
|
737
|
+
}
|
|
738
|
+
if (result.status === "verified") {
|
|
739
|
+
(_d = (_c = this.config).onComplete) == null ? void 0 : _d.call(_c, result);
|
|
740
|
+
} else {
|
|
741
|
+
(_f = (_e = this.config).onError) == null ? void 0 : _f.call(
|
|
742
|
+
_e,
|
|
743
|
+
new Error(`Verification failed: ${result.status}`)
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
window.addEventListener("message", this.messageListener);
|
|
748
|
+
this.popupMonitorInterval = setInterval(() => {
|
|
749
|
+
var _a2, _b2;
|
|
750
|
+
if (this.popupWindow && this.popupWindow.closed) {
|
|
751
|
+
logSecurityEvent("POPUP_CLOSED_BY_USER", {
|
|
752
|
+
sessionId: sessionId.substring(0, 8) + "...",
|
|
753
|
+
environment: this.config.environment
|
|
754
|
+
});
|
|
755
|
+
this.cleanup();
|
|
756
|
+
this.unlockVerification();
|
|
757
|
+
(_b2 = (_a2 = this.config).onCancel) == null ? void 0 : _b2.call(_a2);
|
|
758
|
+
}
|
|
759
|
+
}, 500);
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Set up automatic cleanup on page unload to prevent memory leaks
|
|
763
|
+
*
|
|
764
|
+
* Registers event listeners for page unload events to ensure proper
|
|
765
|
+
* cleanup of resources and verification state. Handles both traditional
|
|
766
|
+
* page navigation and single-page application route changes.
|
|
767
|
+
*
|
|
768
|
+
* @private
|
|
769
|
+
*/
|
|
770
|
+
setupAutoCleanup() {
|
|
771
|
+
this.unloadListener = () => {
|
|
772
|
+
logSecurityEvent("SDK_AUTO_CLEANUP", {
|
|
773
|
+
environment: this.config.environment,
|
|
774
|
+
trigger: "page_unload"
|
|
775
|
+
});
|
|
776
|
+
this.cleanup();
|
|
777
|
+
this.unlockVerification();
|
|
778
|
+
};
|
|
779
|
+
window.addEventListener("beforeunload", this.unloadListener);
|
|
780
|
+
window.addEventListener("pagehide", this.unloadListener);
|
|
781
|
+
if (window.history && window.history.pushState) {
|
|
782
|
+
const originalPushState = window.history.pushState;
|
|
783
|
+
window.history.pushState = (...args) => {
|
|
784
|
+
this.cleanup();
|
|
785
|
+
this.unlockVerification();
|
|
786
|
+
return originalPushState.apply(window.history, args);
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Auto-detect environment based on current URL
|
|
792
|
+
*
|
|
793
|
+
* Analyzes the current hostname to determine the appropriate environment
|
|
794
|
+
* configuration. Used when environment is not explicitly specified.
|
|
795
|
+
* Always defaults to production unless staging is detected.
|
|
796
|
+
*
|
|
797
|
+
* @returns {'production' | 'staging'} Detected environment
|
|
798
|
+
* @private
|
|
799
|
+
*/
|
|
800
|
+
detectEnvironment() {
|
|
801
|
+
const hostname = window.location.hostname;
|
|
802
|
+
if (hostname.includes("staging") || hostname.includes("stage")) {
|
|
803
|
+
return "staging";
|
|
804
|
+
}
|
|
805
|
+
return "production";
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Get the current environment
|
|
809
|
+
* @returns {string} The current environment (production or staging)
|
|
810
|
+
*/
|
|
811
|
+
getEnvironment() {
|
|
812
|
+
return this.config.environment;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Unlock verification process to allow new verifications
|
|
816
|
+
*
|
|
817
|
+
* Resets the verification lock state to allow new verification attempts.
|
|
818
|
+
* Called after successful completion, errors, or cleanup.
|
|
819
|
+
*
|
|
820
|
+
* @private
|
|
821
|
+
*/
|
|
822
|
+
unlockVerification() {
|
|
823
|
+
this.isVerificationInProgress = false;
|
|
824
|
+
this.currentSessionId = null;
|
|
825
|
+
logSecurityEvent("VERIFICATION_UNLOCKED", {
|
|
826
|
+
environment: this.config.environment,
|
|
827
|
+
origin: window.location.origin
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Internal cleanup method to prevent memory leaks
|
|
832
|
+
*
|
|
833
|
+
* Cleans up popup windows, event listeners, and intervals.
|
|
834
|
+
* Does not unlock verification state - that's handled by specific callers.
|
|
835
|
+
*
|
|
836
|
+
* @private
|
|
837
|
+
*/
|
|
838
|
+
cleanup() {
|
|
839
|
+
if (this.popupWindow && !this.popupWindow.closed) {
|
|
840
|
+
this.popupWindow.close();
|
|
841
|
+
}
|
|
842
|
+
this.popupWindow = null;
|
|
843
|
+
if (this.messageListener) {
|
|
844
|
+
window.removeEventListener("message", this.messageListener);
|
|
845
|
+
this.messageListener = null;
|
|
846
|
+
}
|
|
847
|
+
if (this.popupMonitorInterval) {
|
|
848
|
+
clearInterval(this.popupMonitorInterval);
|
|
849
|
+
this.popupMonitorInterval = null;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Remove auto-cleanup listeners
|
|
854
|
+
*
|
|
855
|
+
* Removes page unload event listeners that were set up for automatic cleanup.
|
|
856
|
+
*
|
|
857
|
+
* @private
|
|
858
|
+
*/
|
|
859
|
+
removeAutoCleanupListeners() {
|
|
860
|
+
if (this.unloadListener) {
|
|
861
|
+
window.removeEventListener("beforeunload", this.unloadListener);
|
|
862
|
+
window.removeEventListener("pagehide", this.unloadListener);
|
|
863
|
+
this.unloadListener = null;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Public cleanup method for manual resource management
|
|
868
|
+
*
|
|
869
|
+
* Completely destroys the SDK instance, cleaning up all resources and
|
|
870
|
+
* removing all event listeners. Should be called when the SDK is no longer needed.
|
|
871
|
+
*
|
|
872
|
+
* @public
|
|
873
|
+
*/
|
|
874
|
+
destroy() {
|
|
875
|
+
logSecurityEvent("SDK_DESTROYED", {
|
|
876
|
+
environment: this.config.environment,
|
|
877
|
+
origin: window.location.origin
|
|
878
|
+
});
|
|
879
|
+
this.cleanup();
|
|
880
|
+
this.unlockVerification();
|
|
881
|
+
this.removeAutoCleanupListeners();
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Get Portal API URL based on environment
|
|
885
|
+
*
|
|
886
|
+
* Returns the appropriate portal-api URL for the current environment.
|
|
887
|
+
*
|
|
888
|
+
* @returns {string} Portal API URL
|
|
889
|
+
* @private
|
|
890
|
+
*/
|
|
891
|
+
getPortalApiUrl() {
|
|
892
|
+
switch (this.config.environment) {
|
|
893
|
+
case "staging":
|
|
894
|
+
return "https://api.staging.safepassageapp.com";
|
|
895
|
+
case "production":
|
|
896
|
+
return "https://api.safepassageapp.com";
|
|
897
|
+
default:
|
|
898
|
+
return "https://api.safepassageapp.com";
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Get Engine URL based on environment
|
|
903
|
+
*
|
|
904
|
+
* Returns the appropriate verify-engine URL for the current environment.
|
|
905
|
+
* In production, engine access is proxied through portal-api.
|
|
906
|
+
*
|
|
907
|
+
* @returns {string} Engine URL
|
|
908
|
+
* @private
|
|
909
|
+
*/
|
|
910
|
+
getEngineUrl() {
|
|
911
|
+
switch (this.config.environment) {
|
|
912
|
+
case "staging":
|
|
913
|
+
return "https://engine.staging.safepassageapp.com";
|
|
914
|
+
case "production":
|
|
915
|
+
return "https://engine.safepassageapp.com";
|
|
916
|
+
default:
|
|
917
|
+
return "https://engine.safepassageapp.com";
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Get WebSocket URL based on environment
|
|
922
|
+
*
|
|
923
|
+
* Returns the appropriate WebSocket URL for the current environment.
|
|
924
|
+
* In production, WebSocket connections are proxied through portal-api.
|
|
925
|
+
*
|
|
926
|
+
* @returns {string} WebSocket URL
|
|
927
|
+
* @private
|
|
928
|
+
*/
|
|
929
|
+
getWebSocketUrl() {
|
|
930
|
+
switch (this.config.environment) {
|
|
931
|
+
case "staging":
|
|
932
|
+
return "wss://engine.staging.safepassageapp.com/api/websocket/stream";
|
|
933
|
+
case "production":
|
|
934
|
+
return "wss://engine.safepassageapp.com/api/websocket/stream";
|
|
935
|
+
default:
|
|
936
|
+
return "wss://engine.safepassageapp.com/api/websocket/stream";
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Detect if this is a public key (pk_ prefix) vs private key (sk_ prefix)
|
|
941
|
+
*
|
|
942
|
+
* Determines API key type based on prefix to handle different authentication flows.
|
|
943
|
+
*
|
|
944
|
+
* @returns {boolean} True if public key, false if private key
|
|
945
|
+
* @private
|
|
946
|
+
*/
|
|
947
|
+
isPublicKey() {
|
|
948
|
+
return this.config.apiKey.startsWith("pk_");
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Create session internally for public keys
|
|
952
|
+
*
|
|
953
|
+
* Creates a verification session via the portal API for public key authentication.
|
|
954
|
+
* Generates a UUID session ID and submits session creation request with
|
|
955
|
+
* verification parameters.
|
|
956
|
+
*
|
|
957
|
+
* @param {VerificationOptions} options - Verification options
|
|
958
|
+
* @returns {Promise<string>} Created session ID
|
|
959
|
+
* @throws {Error} If session creation fails
|
|
960
|
+
* @private
|
|
961
|
+
*/
|
|
962
|
+
async createInternalSession(options) {
|
|
963
|
+
var _a, _b;
|
|
964
|
+
try {
|
|
965
|
+
const portalApiUrl = this.getPortalApiUrl();
|
|
966
|
+
const response = await fetch(`${portalApiUrl}/api/v1/sessions/create`, {
|
|
967
|
+
method: "POST",
|
|
968
|
+
headers: {
|
|
969
|
+
"Content-Type": "application/json",
|
|
970
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
971
|
+
},
|
|
972
|
+
body: JSON.stringify({
|
|
973
|
+
merchantId: this.config.apiKey,
|
|
974
|
+
// API expects merchantId even though it uses auth header
|
|
975
|
+
returnUrl: this.config.returnUrl,
|
|
976
|
+
cancelUrl: this.config.cancelUrl,
|
|
977
|
+
challengeAge: options.challengeAge,
|
|
978
|
+
verificationMode: options.verificationMode,
|
|
979
|
+
merchantName: document.title || window.location.hostname,
|
|
980
|
+
externalUserId: options.externalUserId
|
|
981
|
+
})
|
|
982
|
+
});
|
|
983
|
+
if (!response.ok) {
|
|
984
|
+
const errorData = await response.json().catch(() => ({}));
|
|
985
|
+
throw new Error(
|
|
986
|
+
`Failed to create session: ${response.status} ${response.statusText}. ${errorData.message || ""}`
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
const sessionData = await response.json();
|
|
990
|
+
const sessionId = sessionData.sessionId;
|
|
991
|
+
if (!sessionId) {
|
|
992
|
+
throw new Error("Server did not return a sessionId");
|
|
993
|
+
}
|
|
994
|
+
if (sessionData.verifyUrl) {
|
|
995
|
+
this.lastVerifyUrl = sessionData.verifyUrl;
|
|
996
|
+
}
|
|
997
|
+
if (sessionData.sessionToken) {
|
|
998
|
+
this.lastSessionToken = sessionData.sessionToken;
|
|
999
|
+
}
|
|
1000
|
+
if (sessionData.handoffToken) {
|
|
1001
|
+
this.temporaryHandoffToken = sessionData.handoffToken;
|
|
1002
|
+
}
|
|
1003
|
+
logSecurityEvent("INTERNAL_SESSION_CREATED", {
|
|
1004
|
+
sessionId: sessionId.substring(0, 8) + "...",
|
|
1005
|
+
environment: this.config.environment,
|
|
1006
|
+
apiKeyType: "public"
|
|
1007
|
+
});
|
|
1008
|
+
return sessionId;
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1011
|
+
logSecurityEvent("INTERNAL_SESSION_FAILED", {
|
|
1012
|
+
error: errorMessage,
|
|
1013
|
+
environment: this.config.environment,
|
|
1014
|
+
apiKeyType: "public"
|
|
1015
|
+
});
|
|
1016
|
+
(_b = (_a = this.config).onError) == null ? void 0 : _b.call(_a, error);
|
|
1017
|
+
throw new Error(`Failed to create verification session: ${errorMessage}`);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
SafePassageSDK_default = SafePassage;
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// src-redirect/utils/polyfills.ts
|
|
1026
|
+
function setupPolyfills() {
|
|
1027
|
+
if (!crypto.randomUUID) {
|
|
1028
|
+
crypto.randomUUID = function() {
|
|
1029
|
+
const array = new Uint8Array(16);
|
|
1030
|
+
crypto.getRandomValues(array);
|
|
1031
|
+
array[6] = array[6] & 15 | 64;
|
|
1032
|
+
array[8] = array[8] & 63 | 128;
|
|
1033
|
+
const hex = Array.from(array).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1034
|
+
return [
|
|
1035
|
+
hex.slice(0, 8),
|
|
1036
|
+
hex.slice(8, 12),
|
|
1037
|
+
hex.slice(12, 16),
|
|
1038
|
+
hex.slice(16, 20),
|
|
1039
|
+
hex.slice(20, 32)
|
|
1040
|
+
].join("-");
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
function checkBrowserCompatibility() {
|
|
1045
|
+
const warnings = [];
|
|
1046
|
+
if (!window.crypto || !window.crypto.getRandomValues) {
|
|
1047
|
+
throw new Error("SafePassage SDK requires Web Crypto API support");
|
|
1048
|
+
}
|
|
1049
|
+
if (!window.crypto.subtle) {
|
|
1050
|
+
throw new Error(
|
|
1051
|
+
"SafePassage SDK requires Web Crypto subtle API for HMAC operations"
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
if (!crypto.randomUUID) {
|
|
1055
|
+
warnings.push("crypto.randomUUID not supported, using polyfill");
|
|
1056
|
+
}
|
|
1057
|
+
if (!window.URLSearchParams) {
|
|
1058
|
+
warnings.push(
|
|
1059
|
+
"URLSearchParams not supported, consider adding a polyfill for IE 11 support"
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
if (warnings.length > 0) {
|
|
1063
|
+
console.warn("SafePassage SDK Browser Compatibility:", warnings.join("; "));
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src-redirect/index.ts
|
|
1068
|
+
init_SafePassageSDK();
|
|
1069
|
+
if (typeof window !== "undefined") {
|
|
1070
|
+
setupPolyfills();
|
|
1071
|
+
checkBrowserCompatibility();
|
|
1072
|
+
}
|
|
1073
|
+
var VERSION = "3.4.4";
|
|
1074
|
+
if (typeof window !== "undefined" && window) {
|
|
1075
|
+
const { SafePassage: SafePassage2 } = (init_SafePassageSDK(), __toCommonJS(SafePassageSDK_exports));
|
|
1076
|
+
const globalWindow = window;
|
|
1077
|
+
globalWindow.SafePassage = SafePassage2;
|
|
1078
|
+
if (globalWindow.SafePassage) {
|
|
1079
|
+
globalWindow.SafePassage.VERSION = VERSION;
|
|
1080
|
+
}
|
|
40
1081
|
}
|
|
1082
|
+
export {
|
|
1083
|
+
SafePassage,
|
|
1084
|
+
VERSION,
|
|
1085
|
+
SafePassage as default
|
|
1086
|
+
};
|
package/dist/safepassage.min.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/* SafePassage SDK v3.4.
|
|
2
|
-
"use strict";var SafePassageSDK=(()=>{var v=Object.defineProperty,se=Object.defineProperties,oe=Object.getOwnPropertyDescriptor,ae=Object.getOwnPropertyDescriptors,ce=Object.getOwnPropertyNames,w=Object.getOwnPropertySymbols;var I=Object.prototype.hasOwnProperty,R=Object.prototype.propertyIsEnumerable;var L=(t,e,n)=>e in t?v(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,h=(t,e)=>{for(var n in e||(e={}))I.call(e,n)&&L(t,n,e[n]);if(w)for(var n of w(e))R.call(e,n)&&L(t,n,e[n]);return t},S=(t,e)=>se(t,ae(e));var C=(t,e)=>{var n={};for(var i in t)I.call(t,i)&&e.indexOf(i)<0&&(n[i]=t[i]);if(t!=null&&w)for(var i of w(t))e.indexOf(i)<0&&R.call(t,i)&&(n[i]=t[i]);return n};var m=(t,e)=>()=>(t&&(e=t(t=0)),e);var P=(t,e)=>{for(var n in e)v(t,n,{get:e[n],enumerable:!0})},le=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of ce(e))!I.call(t,r)&&r!==n&&v(t,r,{get:()=>e[r],enumerable:!(i=oe(e,r))||i.enumerable});return t};var D=t=>le(v({},"__esModule",{value:!0}),t);function pe(t,e){return N[e].includes(t)}function K(t,e,n=[]){var r;let{origin:i}=t;return pe(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:N[e],allowedCustomOrigins:n,eventType:(r=t.data)==null?void 0:r.type}),!1)}function W(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"].includes(n.status)?{isValid:!1,error:"Invalid status value"}:{isValid:!0}}function H(t){t==="production"&&window.location.protocol!=="https:"&&console.warn("SafePassage Warning: HTTPS recommended for production environment",{current:window.location.href})}function A(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 N,U,j,T=m(()=>{"use strict";N={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"]};U=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 U});var B={};P(B,{createSignedState:()=>ue,generateHMAC:()=>b,generateSecureToken:()=>F,getSigningSecret:()=>x,parseSignedState:()=>ge,verifyHMAC:()=>q});async function b(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 q(t,e,n){try{let i=await b(t,n);return de(e,i)}catch(i){return!1}}function de(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 F(t=32){let e=new Uint8Array(t);return crypto.getRandomValues(e),Array.from(e,n=>n.toString(16).padStart(2,"0")).join("")}function x(t){return{production:"safepassage-prod-hmac-2025",staging:"safepassage-stage-hmac-2025"}[t]}async function ue(t,e){let n=S(h({},t),{timestamp:Date.now(),nonce:F(16)}),i=JSON.stringify(n),r=x(e),s=await b(i,r);return btoa(JSON.stringify({data:n,signature:s}))}async function ge(t,e,n=J){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=x(e);if(!await q(p,a,c))return console.warn("SafePassage: State signature verification failed"),null;if(o.timestamp){let _=Date.now()-o.timestamp;if(_>n)return console.warn("SafePassage: State parameter expired",{age:_,maxAge:n}),null}let i=o,{timestamp:g,nonce:f}=i;return C(i,["timestamp","nonce"])}catch(r){return console.warn("SafePassage: Failed to parse signed state",r),null}}var G=m(()=>{"use strict";V()});function Z(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(!fe.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>y)throw new Error(`returnUrl exceeds maximum length of ${y} characters`);let e=he(),n=A(t.returnUrl,e);if(!n.isValid)throw new Error(`returnUrl validation failed: ${n.error}`);if(t.cancelUrl){if(t.cancelUrl.length>y)throw new Error(`cancelUrl exceeds maximum length of ${y} characters`);let i=A(t.cancelUrl,e);if(!i.isValid)throw new Error(`cancelUrl validation failed: ${i.error}`)}if(t.defaultChallengeAge!==void 0){if(t.defaultChallengeAge<X)throw new Error(`defaultChallengeAge must be at least ${X}`);if(t.defaultChallengeAge>Y)throw new Error(`defaultChallengeAge cannot exceed ${Y}`)}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 he(){if(typeof window=="undefined")return"production";let t=window.location.hostname;return t.includes("staging")||t.includes("stage")?"staging":"production"}async function Q(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(G(),B));return n(t,e)}var X,Y,y,z,J,fe,V=m(()=>{"use strict";T();X=25,Y=150,y=2048,z=128,J=6e5,fe=/^(pk_|sk_)[a-zA-Z0-9_]+$/});function k(t){let e=ee[t]||ee.production;if(!e||!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function me(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 te(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{k(t),me(t)}catch(n){let i=n instanceof Error?n.message:String(n);throw new Error(`Environment configuration validation failed: ${i}`)}}var ee,ne=m(()=>{"use strict";ee={production:"https://av.safepassageapp.com",staging:"https://av.staging.safepassageapp.com"}});var ie={};P(ie,{SafePassage:()=>u,default:()=>we});var u,we,M=m(()=>{"use strict";V();ne();T();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;Z(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=S(h({},e),{environment:n,mode:e.mode||"redirect"}),te(this.config.environment),H(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=k(this.config.environment),r=e.challengeAge!==void 0,s=e.verificationMode!==void 0,o=r||s,a=await Q({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,E;if(!K(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=W(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):(E=(f=this.config).onError)==null||E.call(f,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}`)}}},we=u});var ve={};P(ve,{SafePassage:()=>u,VERSION:()=>re,default:()=>u});function $(){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 O(){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("; "))}M();typeof window!="undefined"&&($(),O());var re="3.4.2";if(typeof window!="undefined"&&window){let{SafePassage:t}=(M(),D(ie)),e=window;e.SafePassage=t,e.SafePassage&&(e.SafePassage.VERSION=re)}return D(ve);})();
|
|
1
|
+
/* SafePassage SDK v3.4.4 */
|
|
2
|
+
"use strict";var SafePassageSDK=(()=>{var v=Object.defineProperty,se=Object.defineProperties,oe=Object.getOwnPropertyDescriptor,ae=Object.getOwnPropertyDescriptors,ce=Object.getOwnPropertyNames,w=Object.getOwnPropertySymbols;var I=Object.prototype.hasOwnProperty,R=Object.prototype.propertyIsEnumerable;var _=(t,e,n)=>e in t?v(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,h=(t,e)=>{for(var n in e||(e={}))I.call(e,n)&&_(t,n,e[n]);if(w)for(var n of w(e))R.call(e,n)&&_(t,n,e[n]);return t},S=(t,e)=>se(t,ae(e));var C=(t,e)=>{var n={};for(var i in t)I.call(t,i)&&e.indexOf(i)<0&&(n[i]=t[i]);if(t!=null&&w)for(var i of w(t))e.indexOf(i)<0&&R.call(t,i)&&(n[i]=t[i]);return n};var m=(t,e)=>()=>(t&&(e=t(t=0)),e);var P=(t,e)=>{for(var n in e)v(t,n,{get:e[n],enumerable:!0})},le=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of ce(e))!I.call(t,s)&&s!==n&&v(t,s,{get:()=>e[s],enumerable:!(i=oe(e,s))||i.enumerable});return t};var D=t=>le(v({},"__esModule",{value:!0}),t);function pe(t,e){return N[e].includes(t)}function K(t,e,n=[]){var s;let{origin:i}=t;return pe(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:N[e],allowedCustomOrigins:n,eventType:(s=t.data)==null?void 0:s.type}),!1)}function W(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"].includes(n.status)?{isValid:!1,error:"Invalid status value"}:{isValid:!0}}function H(t){t==="production"&&window.location.protocol!=="https:"&&console.warn("SafePassage Warning: HTTPS recommended for production environment",{current:window.location.href})}function A(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 s of i)if(s.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 N,U,j,T=m(()=>{"use strict";N={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"]};U=class{constructor(){this.attempts=new Map;this.maxAttempts=5;this.timeWindow=6e4}isAllowed(e){let n=Date.now(),s=(this.attempts.get(e)||[]).filter(r=>n-r<this.timeWindow);return s.length>=this.maxAttempts?(console.warn(`SafePassage Security: Rate limit exceeded for ${e}`),!1):(s.push(n),this.attempts.set(e,s),!0)}reset(e){this.attempts.delete(e)}},j=new U});var B={};P(B,{createSignedState:()=>ue,generateHMAC:()=>b,generateSecureToken:()=>F,getSigningSecret:()=>x,parseSignedState:()=>ge,verifyHMAC:()=>q});async function b(t,e){let n=new TextEncoder,i=n.encode(e),s=n.encode(t),r=await crypto.subtle.importKey("raw",i,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),o=await crypto.subtle.sign("HMAC",r,s);return Array.from(new Uint8Array(o)).map(a=>a.toString(16).padStart(2,"0")).join("")}async function q(t,e,n){try{let i=await b(t,n);return de(e,i)}catch(i){return!1}}function de(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 F(t=32){let e=new Uint8Array(t);return crypto.getRandomValues(e),Array.from(e,n=>n.toString(16).padStart(2,"0")).join("")}function x(t){return{production:"safepassage-prod-hmac-2025",staging:"safepassage-stage-hmac-2025"}[t]}async function ue(t,e){let n=S(h({},t),{timestamp:Date.now(),nonce:F(16)}),i=JSON.stringify(n),s=x(e),r=await b(i,s);return btoa(JSON.stringify({data:n,signature:r}))}async function ge(t,e,n=J){try{let s=atob(t),r=JSON.parse(s);if(!r.data||!r.signature)return console.warn("SafePassage: Invalid signed state format"),null;let{data:o,signature:a}=r,p=JSON.stringify(o),c=x(e);if(!await q(p,a,c))return console.warn("SafePassage: State signature verification failed"),null;if(o.timestamp){let L=Date.now()-o.timestamp;if(L>n)return console.warn("SafePassage: State parameter expired",{age:L,maxAge:n}),null}let i=o,{timestamp:g,nonce:f}=i;return C(i,["timestamp","nonce"])}catch(s){return console.warn("SafePassage: Failed to parse signed state",s),null}}var G=m(()=>{"use strict";V()});function Z(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(!fe.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>y)throw new Error(`returnUrl exceeds maximum length of ${y} characters`);let e=he(),n=A(t.returnUrl,e);if(!n.isValid)throw new Error(`returnUrl validation failed: ${n.error}`);if(t.cancelUrl){if(t.cancelUrl.length>y)throw new Error(`cancelUrl exceeds maximum length of ${y} characters`);let i=A(t.cancelUrl,e);if(!i.isValid)throw new Error(`cancelUrl validation failed: ${i.error}`)}if(t.defaultChallengeAge!==void 0){if(t.defaultChallengeAge<X)throw new Error(`defaultChallengeAge must be at least ${X}`);if(t.defaultChallengeAge>Y)throw new Error(`defaultChallengeAge cannot exceed ${Y}`)}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 he(){if(typeof window=="undefined")return"production";let t=window.location.hostname;return t.includes("staging")||t.includes("stage")?"staging":"production"}async function Q(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(G(),B));return n(t,e)}var X,Y,y,z,J,fe,V=m(()=>{"use strict";T();X=25,Y=150,y=2048,z=128,J=6e5,fe=/^(pk_|sk_)[a-zA-Z0-9_]+$/});function k(t){let e=ee[t]||ee.production;if(!e||!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function me(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 te(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{k(t),me(t)}catch(n){let i=n instanceof Error?n.message:String(n);throw new Error(`Environment configuration validation failed: ${i}`)}}var ee,ne=m(()=>{"use strict";ee={production:"https://av.safepassageapp.com",staging:"https://av.staging.safepassageapp.com"}});var ie={};P(ie,{SafePassage:()=>u,default:()=>we});var u,we,M=m(()=>{"use strict";V();ne();T();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;this.temporaryHandoffToken=null;Z(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=S(h({},e),{environment:n,mode:e.mode||"redirect"}),te(this.config.environment),H(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 s,r,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 ${(s=this.currentSessionId)==null?void 0:s.substring(0,8)}...`);throw l("RACE_CONDITION_PREVENTED",{currentSession:((r=this.currentSessionId)==null?void 0:r.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=k(this.config.environment),s=e.challengeAge!==void 0,r=e.verificationMode!==void 0,o=s||r,a=await Q({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||void 0,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,s;if(this.cleanup(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),this.popupWindow=window.open(e,"safepassage-verify","width=600,height=700"),!this.popupWindow){(s=(i=this.config).onError)==null||s.call(i,new Error("Failed to open verification window. Please check popup blocker settings."));return}this.messageListener=r=>{var p,c,d,g,f,E;if(!K(r,this.config.environment)){l("POSTMESSAGE_ORIGIN_BLOCKED",{origin:r.origin,environment:this.config.environment,expectedOrigins:`SafePassage trusted origins for ${this.config.environment}`,messageType:(p=r.data)==null?void 0:p.type});return}let o=W(r,n);if(!o.isValid){l("POSTMESSAGE_VALIDATION_FAILED",{error:o.error,origin:r.origin,sessionId:n.substring(0,8)+"...",messageType:(c=r.data)==null?void 0:c.type});return}let a={sessionId:r.data.sessionId,status:r.data.status,timestamp:r.data.timestamp,externalUserId:r.data.externalUserId};l("VERIFICATION_COMPLETED",{status:a.status,sessionId:n.substring(0,8)+"...",origin:r.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):(E=(f=this.config).onError)==null||E.call(f,new Error(`Verification failed: ${a.status}`))},window.addEventListener("message",this.messageListener),this.popupMonitorInterval=setInterval(()=>{var r,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=(r=this.config).onCancel)==null||o.call(r))},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 s=this.getPortalApiUrl(),r=await fetch(`${s}/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(!r.ok){let p=await r.json().catch(()=>({}));throw new Error(`Failed to create session: ${r.status} ${r.statusText}. ${p.message||""}`)}let o=await r.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(s){let r=s instanceof Error?s.message:String(s);throw l("INTERNAL_SESSION_FAILED",{error:r,environment:this.config.environment,apiKeyType:"public"}),(i=(n=this.config).onError)==null||i.call(n,s),new Error(`Failed to create verification session: ${r}`)}}},we=u});var ve={};P(ve,{SafePassage:()=>u,VERSION:()=>re,default:()=>u});function $(){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 O(){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("; "))}M();typeof window!="undefined"&&($(),O());var re="3.4.4";if(typeof window!="undefined"&&window){let{SafePassage:t}=(M(),D(ie)),e=window;e.SafePassage=t,e.SafePassage&&(e.SafePassage.VERSION=re)}return D(ve);})();
|
|
3
3
|
if(typeof SafePassageSDK !== "undefined" && SafePassageSDK.SafePassage) { window.SafePassage = SafePassageSDK.SafePassage; window.SafePassage.VERSION = SafePassageSDK.VERSION; }
|
package/dist/types/index.d.ts
CHANGED
|
@@ -92,6 +92,10 @@ export interface VerificationResult {
|
|
|
92
92
|
* Full details available via server-side API
|
|
93
93
|
*/
|
|
94
94
|
status: 'verified' | 'failed';
|
|
95
|
+
/**
|
|
96
|
+
* Timestamp when verification completed (milliseconds since epoch)
|
|
97
|
+
*/
|
|
98
|
+
timestamp?: number;
|
|
95
99
|
/**
|
|
96
100
|
* External user identifier if provided during verification
|
|
97
101
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@safepassage/sdk",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.4",
|
|
4
4
|
"description": "SafePassage SDK - Lightweight redirect-based age verification",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"LICENSE"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "tsc && node build-sdk.js",
|
|
13
|
+
"build": "npm run clean && tsc && node build-sdk.js",
|
|
14
|
+
"clean": "rm -rf dist",
|
|
14
15
|
"dev": "tsc --watch",
|
|
15
16
|
"demo": "python3 -m http.server 8080",
|
|
16
17
|
"serve-demo": "npx serve . -p 8080",
|