@safepassage/sdk 3.4.9 → 3.4.10
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 +14 -10
- package/brands/safepassage/index.d.ts +12 -0
- package/brands/safepassage/urls.d.ts +3 -0
- package/core/VerificationSDK.d.ts +132 -0
- package/index.d.ts +2 -0
- package/index.js +1091 -0
- package/package.json +14 -47
- package/safepassage.min.js +3 -0
- package/sdk.min.js +3 -0
- package/{dist/types/index.d.ts → types/base.d.ts} +27 -10
- package/utils/crypto.d.ts +31 -0
- package/utils/environment.d.ts +8 -0
- package/{dist/utils → utils}/polyfills.d.ts +1 -2
- package/{dist/utils → utils}/security.d.ts +11 -11
- package/{dist/utils → utils}/validation.d.ts +9 -12
- package/dist/core/SafePassageSDK.d.ts +0 -231
- package/dist/core/SafePassageSDK.js +0 -656
- package/dist/index.d.ts +0 -21
- package/dist/index.js +0 -1100
- package/dist/safepassage.min.js +0 -3
- package/dist/types/index.js +0 -4
- package/dist/utils/__mocks__/polyfills.d.ts +0 -3
- package/dist/utils/__mocks__/polyfills.js +0 -10
- package/dist/utils/crypto.d.ts +0 -105
- package/dist/utils/crypto.js +0 -216
- package/dist/utils/environment.d.ts +0 -13
- package/dist/utils/environment.js +0 -75
- package/dist/utils/polyfills.js +0 -58
- package/dist/utils/security.js +0 -190
- package/dist/utils/validation.js +0 -158
|
@@ -1,656 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SafePassage SDK - Redirect-based age verification
|
|
3
|
-
*
|
|
4
|
-
* Lightweight SDK for integrating SafePassage age verification using redirect flow.
|
|
5
|
-
* Provides a secure, easy-to-implement solution for age verification with comprehensive
|
|
6
|
-
* security features and flexible integration options.
|
|
7
|
-
*
|
|
8
|
-
* Key Features:
|
|
9
|
-
* - Redirect and new-tab verification modes
|
|
10
|
-
* - Automatic session management for public API keys
|
|
11
|
-
* - HMAC-signed state parameters for security
|
|
12
|
-
* - Rate limiting and race condition protection
|
|
13
|
-
* - Comprehensive security validation and logging
|
|
14
|
-
* - PostMessage communication for new-tab mode
|
|
15
|
-
* - Automatic cleanup and resource management
|
|
16
|
-
* - Environment-specific configuration
|
|
17
|
-
*
|
|
18
|
-
* Security Features:
|
|
19
|
-
* - Origin validation for PostMessage communication
|
|
20
|
-
* - HTTPS enforcement in production
|
|
21
|
-
* - Rate limiting per API key and origin
|
|
22
|
-
* - State parameter signing with timestamps and nonces
|
|
23
|
-
* - Comprehensive security event logging
|
|
24
|
-
* - Protection against race conditions and replay attacks
|
|
25
|
-
*
|
|
26
|
-
* @author SafePassage Engineering
|
|
27
|
-
* @version 2.0.0
|
|
28
|
-
* @since 1.0.0
|
|
29
|
-
*/
|
|
30
|
-
import { generateState, validateConfig } from '../utils/validation';
|
|
31
|
-
import { getEnvironmentUrl, validateEnvironmentSecurity, } from '../utils/environment';
|
|
32
|
-
import { validatePostMessageOrigin, validateSafePassageMessage, enforceHTTPS, verificationRateLimit, logSecurityEvent, } from '../utils/security';
|
|
33
|
-
/**
|
|
34
|
-
* SafePassage SDK Main Class
|
|
35
|
-
*
|
|
36
|
-
* Primary SDK class that manages age verification sessions with comprehensive
|
|
37
|
-
* security and error handling. Supports both redirect and new-tab modes with
|
|
38
|
-
* automatic session management and PostMessage communication.
|
|
39
|
-
*
|
|
40
|
-
* The class implements multiple security layers including origin validation,
|
|
41
|
-
* rate limiting, state signing, and comprehensive event logging to ensure
|
|
42
|
-
* secure verification flows.
|
|
43
|
-
*/
|
|
44
|
-
export class SafePassage {
|
|
45
|
-
/**
|
|
46
|
-
* Initialize SafePassage SDK
|
|
47
|
-
*
|
|
48
|
-
* Validates configuration, sets up security measures, and prepares the SDK
|
|
49
|
-
* for verification operations. Performs comprehensive environment validation
|
|
50
|
-
* and security initialization.
|
|
51
|
-
*
|
|
52
|
-
* @param {SafePassageConfig} config - SDK configuration object
|
|
53
|
-
* @throws {Error} If configuration validation fails
|
|
54
|
-
*/
|
|
55
|
-
constructor(config) {
|
|
56
|
-
this.popupWindow = null;
|
|
57
|
-
this.messageListener = null;
|
|
58
|
-
this.popupMonitorInterval = null;
|
|
59
|
-
this.unloadListener = null;
|
|
60
|
-
this.isVerificationInProgress = false;
|
|
61
|
-
this.currentSessionId = null;
|
|
62
|
-
this.hasReceivedResult = false;
|
|
63
|
-
// Server-provided verify URL (includes sessionToken)
|
|
64
|
-
this.lastVerifyUrl = null;
|
|
65
|
-
// Server-provided session token (WS auth)
|
|
66
|
-
this.lastSessionToken = null;
|
|
67
|
-
// Temporary storage for QR handoff token to include in state
|
|
68
|
-
this.temporaryHandoffToken = null;
|
|
69
|
-
validateConfig(config);
|
|
70
|
-
// Normalize environment: treat 'development' or any unknown value as 'production'
|
|
71
|
-
let normalizedEnvironment = config.environment || this.detectEnvironment();
|
|
72
|
-
if (normalizedEnvironment !== 'staging' && normalizedEnvironment !== 'production') {
|
|
73
|
-
console.warn(`SafePassage SDK: Unknown environment '${normalizedEnvironment}', defaulting to 'production'`);
|
|
74
|
-
normalizedEnvironment = 'production';
|
|
75
|
-
}
|
|
76
|
-
this.config = Object.assign(Object.assign({}, config), { environment: normalizedEnvironment, mode: config.mode || 'redirect' });
|
|
77
|
-
// Comprehensive environment security validation
|
|
78
|
-
validateEnvironmentSecurity(this.config.environment);
|
|
79
|
-
// Enforce HTTPS in production (additional layer)
|
|
80
|
-
enforceHTTPS(this.config.environment);
|
|
81
|
-
// Log initialization for security monitoring
|
|
82
|
-
logSecurityEvent('SDK_INITIALIZED', {
|
|
83
|
-
environment: this.config.environment,
|
|
84
|
-
mode: this.config.mode,
|
|
85
|
-
origin: window.location.origin,
|
|
86
|
-
protocol: window.location.protocol,
|
|
87
|
-
hostname: window.location.hostname,
|
|
88
|
-
});
|
|
89
|
-
// Set up automatic cleanup on page unload
|
|
90
|
-
this.setupAutoCleanup();
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Initiate age verification with race condition protection
|
|
94
|
-
*
|
|
95
|
-
* Main verification method that handles session creation, security validation,
|
|
96
|
-
* and verification flow initiation. Includes race condition protection and
|
|
97
|
-
* comprehensive error handling.
|
|
98
|
-
*
|
|
99
|
-
* For public keys (pk_*), automatically creates sessions via the portal API.
|
|
100
|
-
* For private keys (sk_*), requires a pre-created sessionId.
|
|
101
|
-
*
|
|
102
|
-
* @param {VerificationOptions} [options={}] - Verification options
|
|
103
|
-
* @param {string} [options.sessionId] - Session ID (required for private keys)
|
|
104
|
-
* @param {number} [options.challengeAge] - Age challenge override
|
|
105
|
-
* @param {string} [options.verificationMode] - Verification mode override
|
|
106
|
-
* @param {string} [options.externalUserId] - External user identifier
|
|
107
|
-
* @returns {Promise<void>} Promise that resolves when verification is initiated
|
|
108
|
-
* @throws {Error} If verification cannot be started or is already in progress
|
|
109
|
-
*/
|
|
110
|
-
async verify(options = {}) {
|
|
111
|
-
var _a, _b, _c, _d, _e, _f;
|
|
112
|
-
const isPublicKey = this.isPublicKey();
|
|
113
|
-
// For public keys, create session via API (SafePassage generates the sessionId)
|
|
114
|
-
let sessionId;
|
|
115
|
-
if (isPublicKey) {
|
|
116
|
-
sessionId = await this.createInternalSession(options);
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
// Private keys should use the direct API, not the SDK
|
|
120
|
-
throw new Error('Private API keys (sk_) should use the direct API, not the SDK. ' +
|
|
121
|
-
'The SDK is designed for browser-based public key usage only.');
|
|
122
|
-
}
|
|
123
|
-
if (!sessionId) {
|
|
124
|
-
throw new Error('Failed to obtain sessionId from server');
|
|
125
|
-
}
|
|
126
|
-
// Race condition check - prevent multiple simultaneous verifications
|
|
127
|
-
if (this.isVerificationInProgress) {
|
|
128
|
-
const error = new Error(`Verification already in progress for session ${(_a = this.currentSessionId) === null || _a === void 0 ? void 0 : _a.substring(0, 8)}...`);
|
|
129
|
-
logSecurityEvent('RACE_CONDITION_PREVENTED', {
|
|
130
|
-
currentSession: ((_b = this.currentSessionId) === null || _b === void 0 ? void 0 : _b.substring(0, 8)) + '...',
|
|
131
|
-
attemptedSession: 'new-session-attempt',
|
|
132
|
-
origin: window.location.origin,
|
|
133
|
-
});
|
|
134
|
-
(_d = (_c = this.config).onError) === null || _d === void 0 ? void 0 : _d.call(_c, error);
|
|
135
|
-
throw error;
|
|
136
|
-
}
|
|
137
|
-
// Lock verification process
|
|
138
|
-
this.isVerificationInProgress = true;
|
|
139
|
-
this.currentSessionId = sessionId;
|
|
140
|
-
try {
|
|
141
|
-
// Rate limiting check
|
|
142
|
-
const rateLimitKey = `${this.config.apiKey}:${window.location.origin}`;
|
|
143
|
-
if (!verificationRateLimit.isAllowed(rateLimitKey)) {
|
|
144
|
-
const error = new Error('Too many verification attempts. Please wait before trying again.');
|
|
145
|
-
logSecurityEvent('RATE_LIMIT_EXCEEDED', {
|
|
146
|
-
apiKey: this.config.apiKey.substring(0, 8) + '...',
|
|
147
|
-
origin: window.location.origin,
|
|
148
|
-
sessionId: sessionId
|
|
149
|
-
? sessionId.substring(0, 8) + '...'
|
|
150
|
-
: 'undefined',
|
|
151
|
-
});
|
|
152
|
-
(_f = (_e = this.config).onError) === null || _f === void 0 ? void 0 : _f.call(_e, error);
|
|
153
|
-
throw error;
|
|
154
|
-
}
|
|
155
|
-
const verificationUrl = await this.buildVerificationUrl(options, sessionId);
|
|
156
|
-
// Log verification attempt
|
|
157
|
-
logSecurityEvent('VERIFICATION_INITIATED', {
|
|
158
|
-
environment: this.config.environment,
|
|
159
|
-
mode: this.config.mode,
|
|
160
|
-
sessionId: sessionId ? sessionId.substring(0, 8) + '...' : 'undefined',
|
|
161
|
-
origin: window.location.origin,
|
|
162
|
-
});
|
|
163
|
-
if (this.config.mode === 'new-tab') {
|
|
164
|
-
this.openNewTab(verificationUrl, sessionId);
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
// For redirect mode, unlock immediately since we're leaving the page
|
|
168
|
-
this.unlockVerification();
|
|
169
|
-
this.redirect(verificationUrl);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
catch (error) {
|
|
173
|
-
// Always unlock on error
|
|
174
|
-
this.unlockVerification();
|
|
175
|
-
throw error;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Build verification URL with HMAC-signed state
|
|
180
|
-
*
|
|
181
|
-
* Constructs the verification URL with signed state parameter containing
|
|
182
|
-
* all necessary configuration and security information. The state parameter
|
|
183
|
-
* includes HMAC signature for integrity protection.
|
|
184
|
-
*
|
|
185
|
-
* @param {VerificationOptions} options - Verification options
|
|
186
|
-
* @returns {Promise<string>} Complete verification URL
|
|
187
|
-
* @private
|
|
188
|
-
*/
|
|
189
|
-
async buildVerificationUrl(options, sessionId) {
|
|
190
|
-
const baseUrl = getEnvironmentUrl(this.config.environment);
|
|
191
|
-
// Determine if we have explicit overrides
|
|
192
|
-
const hasExplicitChallengeAge = options.challengeAge !== undefined;
|
|
193
|
-
const hasExplicitVerificationMode = options.verificationMode !== undefined;
|
|
194
|
-
const hasOverrides = hasExplicitChallengeAge || hasExplicitVerificationMode;
|
|
195
|
-
const state = await generateState({
|
|
196
|
-
merchantId: this.config.apiKey,
|
|
197
|
-
sessionId,
|
|
198
|
-
returnUrl: this.config.returnUrl,
|
|
199
|
-
cancelUrl: this.config.cancelUrl,
|
|
200
|
-
challengeAge: options.challengeAge || this.config.defaultChallengeAge,
|
|
201
|
-
verificationMode: options.verificationMode || this.config.defaultVerificationMode,
|
|
202
|
-
hasOverrides: hasOverrides, // Flag to indicate explicit overrides
|
|
203
|
-
externalUserId: options.externalUserId,
|
|
204
|
-
timestamp: Date.now(),
|
|
205
|
-
// Phase 2: Include config for self-contained verify-ui
|
|
206
|
-
apiUrl: this.getPortalApiUrl(),
|
|
207
|
-
engineUrl: this.getEngineUrl(),
|
|
208
|
-
wsUrl: this.getWebSocketUrl(),
|
|
209
|
-
environment: this.config.environment,
|
|
210
|
-
features: {
|
|
211
|
-
testMode: false,
|
|
212
|
-
warmupPeriodMs: 500,
|
|
213
|
-
qualityThreshold: 0.6,
|
|
214
|
-
},
|
|
215
|
-
// Include handoffToken if available (for QR code desktop flow)
|
|
216
|
-
handoffToken: this.temporaryHandoffToken || undefined,
|
|
217
|
-
// Include sessionToken and verifyUrl to make UI auth deterministic
|
|
218
|
-
sessionToken: this.lastSessionToken || undefined,
|
|
219
|
-
verifyUrl: this.lastVerifyUrl || undefined,
|
|
220
|
-
}, this.config.environment);
|
|
221
|
-
// Prefer server-provided verifyUrl (contains sessionToken) and append state/mode
|
|
222
|
-
if (this.lastVerifyUrl) {
|
|
223
|
-
try {
|
|
224
|
-
const url = new URL(this.lastVerifyUrl);
|
|
225
|
-
url.searchParams.set('state', state);
|
|
226
|
-
url.searchParams.set('mode', this.config.mode);
|
|
227
|
-
// Append skip parameters if provided
|
|
228
|
-
if (options.skipIntro) {
|
|
229
|
-
url.searchParams.set('skip_intro', 'true');
|
|
230
|
-
}
|
|
231
|
-
if (options.autoReturn) {
|
|
232
|
-
url.searchParams.set('auto_return', 'true');
|
|
233
|
-
}
|
|
234
|
-
return url.toString();
|
|
235
|
-
}
|
|
236
|
-
catch (_a) {
|
|
237
|
-
// Fall back to client-constructed URL if parsing fails
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
// Client-constructed URL fallback (for backwards compatibility)
|
|
241
|
-
const params = new URLSearchParams({ state, sessionId, mode: this.config.mode });
|
|
242
|
-
// Append skip parameters if provided
|
|
243
|
-
if (options.skipIntro) {
|
|
244
|
-
params.set('skip_intro', 'true');
|
|
245
|
-
}
|
|
246
|
-
if (options.autoReturn) {
|
|
247
|
-
params.set('auto_return', 'true');
|
|
248
|
-
}
|
|
249
|
-
return `${baseUrl}/?${params.toString()}`;
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Redirect in same tab
|
|
253
|
-
*
|
|
254
|
-
* Performs a full page redirect to the verification URL.
|
|
255
|
-
* Used for redirect mode verification.
|
|
256
|
-
*
|
|
257
|
-
* @param {string} url - Verification URL to redirect to
|
|
258
|
-
* @private
|
|
259
|
-
*/
|
|
260
|
-
redirect(url) {
|
|
261
|
-
window.location.href = url;
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Open in new tab with PostMessage communication and proper cleanup
|
|
265
|
-
*
|
|
266
|
-
* Opens verification URL in a new tab/window and sets up secure PostMessage
|
|
267
|
-
* communication for receiving verification results. Includes comprehensive
|
|
268
|
-
* security validation and automatic cleanup.
|
|
269
|
-
*
|
|
270
|
-
* @param {string} url - Verification URL to open
|
|
271
|
-
* @param {string} sessionId - Session ID for result correlation
|
|
272
|
-
* @private
|
|
273
|
-
*/
|
|
274
|
-
openNewTab(url, sessionId) {
|
|
275
|
-
var _a, _b;
|
|
276
|
-
// Clean up any existing resources
|
|
277
|
-
this.cleanup();
|
|
278
|
-
this.hasReceivedResult = false;
|
|
279
|
-
// Clear any existing popup monitor interval
|
|
280
|
-
if (this.popupMonitorInterval) {
|
|
281
|
-
clearInterval(this.popupMonitorInterval);
|
|
282
|
-
this.popupMonitorInterval = null;
|
|
283
|
-
}
|
|
284
|
-
// Open new tab
|
|
285
|
-
this.popupWindow = window.open(url, 'safepassage-verify', 'width=600,height=700');
|
|
286
|
-
if (!this.popupWindow) {
|
|
287
|
-
(_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.'));
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
// Set up PostMessage listener with enhanced security
|
|
291
|
-
this.messageListener = (event) => {
|
|
292
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
293
|
-
// First: Check if this is a SafePassage message (has our message type prefix)
|
|
294
|
-
// Silently ignore non-SafePassage messages (browser extensions, other libraries)
|
|
295
|
-
const messageType = (_a = event.data) === null || _a === void 0 ? void 0 : _a.type;
|
|
296
|
-
if (!messageType || typeof messageType !== 'string' || !messageType.startsWith('safepassage:')) {
|
|
297
|
-
// Not a SafePassage message - silently ignore
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
// Enhanced origin validation with strict allowlist (only for SafePassage messages)
|
|
301
|
-
if (!validatePostMessageOrigin(event, this.config.environment)) {
|
|
302
|
-
logSecurityEvent('POSTMESSAGE_ORIGIN_BLOCKED', {
|
|
303
|
-
origin: event.origin,
|
|
304
|
-
environment: this.config.environment,
|
|
305
|
-
expectedOrigins: `SafePassage trusted origins for ${this.config.environment}`,
|
|
306
|
-
messageType: (_b = event.data) === null || _b === void 0 ? void 0 : _b.type,
|
|
307
|
-
});
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
// Enhanced message validation
|
|
311
|
-
const messageValidation = validateSafePassageMessage(event, sessionId);
|
|
312
|
-
if (!messageValidation.isValid) {
|
|
313
|
-
logSecurityEvent('POSTMESSAGE_VALIDATION_FAILED', {
|
|
314
|
-
error: messageValidation.error,
|
|
315
|
-
origin: event.origin,
|
|
316
|
-
sessionId: sessionId.substring(0, 8) + '...',
|
|
317
|
-
messageType: (_c = event.data) === null || _c === void 0 ? void 0 : _c.type,
|
|
318
|
-
});
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
const result = {
|
|
322
|
-
sessionId: event.data.sessionId,
|
|
323
|
-
status: event.data.status,
|
|
324
|
-
timestamp: event.data.timestamp,
|
|
325
|
-
externalUserId: event.data.externalUserId,
|
|
326
|
-
};
|
|
327
|
-
this.hasReceivedResult = true;
|
|
328
|
-
// Log successful verification completion
|
|
329
|
-
logSecurityEvent('VERIFICATION_COMPLETED', {
|
|
330
|
-
status: result.status,
|
|
331
|
-
sessionId: sessionId.substring(0, 8) + '...',
|
|
332
|
-
origin: event.origin,
|
|
333
|
-
});
|
|
334
|
-
// Clean up resources but keep the popup open for post-verification actions
|
|
335
|
-
this.cleanup({ closePopup: false });
|
|
336
|
-
// Unlock verification after successful completion
|
|
337
|
-
this.unlockVerification();
|
|
338
|
-
// Clear monitoring interval
|
|
339
|
-
if (this.popupMonitorInterval) {
|
|
340
|
-
clearInterval(this.popupMonitorInterval);
|
|
341
|
-
this.popupMonitorInterval = null;
|
|
342
|
-
}
|
|
343
|
-
// Trigger appropriate callback
|
|
344
|
-
if (result.status === 'verified') {
|
|
345
|
-
(_e = (_d = this.config).onComplete) === null || _e === void 0 ? void 0 : _e.call(_d, result);
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
// Status is 'failed' - trigger error callback
|
|
349
|
-
(_g = (_f = this.config).onError) === null || _g === void 0 ? void 0 : _g.call(_f, new Error(`Verification failed: ${result.status}`));
|
|
350
|
-
}
|
|
351
|
-
};
|
|
352
|
-
window.addEventListener('message', this.messageListener);
|
|
353
|
-
// Monitor popup window with proper cleanup
|
|
354
|
-
this.popupMonitorInterval = setInterval(() => {
|
|
355
|
-
var _a, _b;
|
|
356
|
-
if (this.popupWindow && this.popupWindow.closed) {
|
|
357
|
-
// Log popup closed event
|
|
358
|
-
logSecurityEvent('POPUP_CLOSED_BY_USER', {
|
|
359
|
-
sessionId: sessionId.substring(0, 8) + '...',
|
|
360
|
-
environment: this.config.environment,
|
|
361
|
-
});
|
|
362
|
-
// Clean up and trigger cancel callback
|
|
363
|
-
this.cleanup();
|
|
364
|
-
// Unlock verification after popup closed
|
|
365
|
-
this.unlockVerification();
|
|
366
|
-
if (!this.hasReceivedResult) {
|
|
367
|
-
(_b = (_a = this.config).onCancel) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}, 500);
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Set up automatic cleanup on page unload to prevent memory leaks
|
|
374
|
-
*
|
|
375
|
-
* Registers event listeners for page unload events to ensure proper
|
|
376
|
-
* cleanup of resources and verification state. Handles both traditional
|
|
377
|
-
* page navigation and single-page application route changes.
|
|
378
|
-
*
|
|
379
|
-
* @private
|
|
380
|
-
*/
|
|
381
|
-
setupAutoCleanup() {
|
|
382
|
-
this.unloadListener = () => {
|
|
383
|
-
// Log automatic cleanup
|
|
384
|
-
logSecurityEvent('SDK_AUTO_CLEANUP', {
|
|
385
|
-
environment: this.config.environment,
|
|
386
|
-
trigger: 'page_unload',
|
|
387
|
-
});
|
|
388
|
-
// Clean up all resources and unlock verification
|
|
389
|
-
this.cleanup();
|
|
390
|
-
this.unlockVerification();
|
|
391
|
-
};
|
|
392
|
-
// Listen for page unload events
|
|
393
|
-
window.addEventListener('beforeunload', this.unloadListener);
|
|
394
|
-
window.addEventListener('pagehide', this.unloadListener);
|
|
395
|
-
// For single-page applications, also listen for route changes
|
|
396
|
-
if (window.history && window.history.pushState) {
|
|
397
|
-
const originalPushState = window.history.pushState;
|
|
398
|
-
window.history.pushState = (...args) => {
|
|
399
|
-
this.cleanup();
|
|
400
|
-
this.unlockVerification();
|
|
401
|
-
return originalPushState.apply(window.history, args);
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* Auto-detect environment based on current URL
|
|
407
|
-
*
|
|
408
|
-
* Analyzes the current hostname to determine the appropriate environment
|
|
409
|
-
* configuration. Used when environment is not explicitly specified.
|
|
410
|
-
* Always defaults to production unless staging is detected.
|
|
411
|
-
*
|
|
412
|
-
* @returns {'production' | 'staging'} Detected environment
|
|
413
|
-
* @private
|
|
414
|
-
*/
|
|
415
|
-
detectEnvironment() {
|
|
416
|
-
const hostname = window.location.hostname;
|
|
417
|
-
if (hostname.includes('staging') || hostname.includes('stage')) {
|
|
418
|
-
return 'staging';
|
|
419
|
-
}
|
|
420
|
-
return 'production';
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Get the current environment
|
|
424
|
-
* @returns {string} The current environment (production or staging)
|
|
425
|
-
*/
|
|
426
|
-
getEnvironment() {
|
|
427
|
-
return this.config.environment;
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Unlock verification process to allow new verifications
|
|
431
|
-
*
|
|
432
|
-
* Resets the verification lock state to allow new verification attempts.
|
|
433
|
-
* Called after successful completion, errors, or cleanup.
|
|
434
|
-
*
|
|
435
|
-
* @private
|
|
436
|
-
*/
|
|
437
|
-
unlockVerification() {
|
|
438
|
-
this.isVerificationInProgress = false;
|
|
439
|
-
this.currentSessionId = null;
|
|
440
|
-
logSecurityEvent('VERIFICATION_UNLOCKED', {
|
|
441
|
-
environment: this.config.environment,
|
|
442
|
-
origin: window.location.origin,
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* Internal cleanup method to prevent memory leaks
|
|
447
|
-
*
|
|
448
|
-
* Cleans up popup windows, event listeners, and intervals.
|
|
449
|
-
* Does not unlock verification state - that's handled by specific callers.
|
|
450
|
-
*
|
|
451
|
-
* @private
|
|
452
|
-
*/
|
|
453
|
-
cleanup(options = {}) {
|
|
454
|
-
const shouldClosePopup = options.closePopup !== false;
|
|
455
|
-
// Close popup window unless we're intentionally keeping it open
|
|
456
|
-
if (this.popupWindow) {
|
|
457
|
-
if (shouldClosePopup && !this.popupWindow.closed) {
|
|
458
|
-
this.popupWindow.close();
|
|
459
|
-
}
|
|
460
|
-
if (shouldClosePopup || this.popupWindow.closed) {
|
|
461
|
-
this.popupWindow = null;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
// Remove message listener
|
|
465
|
-
if (this.messageListener) {
|
|
466
|
-
window.removeEventListener('message', this.messageListener);
|
|
467
|
-
this.messageListener = null;
|
|
468
|
-
}
|
|
469
|
-
// Clear monitoring interval
|
|
470
|
-
if (this.popupMonitorInterval) {
|
|
471
|
-
clearInterval(this.popupMonitorInterval);
|
|
472
|
-
this.popupMonitorInterval = null;
|
|
473
|
-
}
|
|
474
|
-
// Note: Verification unlocking is handled by specific callers
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* Remove auto-cleanup listeners
|
|
478
|
-
*
|
|
479
|
-
* Removes page unload event listeners that were set up for automatic cleanup.
|
|
480
|
-
*
|
|
481
|
-
* @private
|
|
482
|
-
*/
|
|
483
|
-
removeAutoCleanupListeners() {
|
|
484
|
-
if (this.unloadListener) {
|
|
485
|
-
window.removeEventListener('beforeunload', this.unloadListener);
|
|
486
|
-
window.removeEventListener('pagehide', this.unloadListener);
|
|
487
|
-
this.unloadListener = null;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* Public cleanup method for manual resource management
|
|
492
|
-
*
|
|
493
|
-
* Completely destroys the SDK instance, cleaning up all resources and
|
|
494
|
-
* removing all event listeners. Should be called when the SDK is no longer needed.
|
|
495
|
-
*
|
|
496
|
-
* @public
|
|
497
|
-
*/
|
|
498
|
-
destroy() {
|
|
499
|
-
// Log destruction for security monitoring
|
|
500
|
-
logSecurityEvent('SDK_DESTROYED', {
|
|
501
|
-
environment: this.config.environment,
|
|
502
|
-
origin: window.location.origin,
|
|
503
|
-
});
|
|
504
|
-
// Clean up all resources
|
|
505
|
-
this.cleanup();
|
|
506
|
-
// Unlock verification
|
|
507
|
-
this.unlockVerification();
|
|
508
|
-
// Remove auto-cleanup listeners
|
|
509
|
-
this.removeAutoCleanupListeners();
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Get Portal API URL based on environment
|
|
513
|
-
*
|
|
514
|
-
* Returns the appropriate portal-api URL for the current environment.
|
|
515
|
-
*
|
|
516
|
-
* @returns {string} Portal API URL
|
|
517
|
-
* @private
|
|
518
|
-
*/
|
|
519
|
-
getPortalApiUrl() {
|
|
520
|
-
switch (this.config.environment) {
|
|
521
|
-
case 'staging':
|
|
522
|
-
return 'https://api.staging.safepassageapp.com';
|
|
523
|
-
case 'production':
|
|
524
|
-
return 'https://api.safepassageapp.com';
|
|
525
|
-
default:
|
|
526
|
-
return 'https://api.safepassageapp.com';
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
/**
|
|
530
|
-
* Get Engine URL based on environment
|
|
531
|
-
*
|
|
532
|
-
* Returns the appropriate verify-engine URL for the current environment.
|
|
533
|
-
* In production, engine access is proxied through portal-api.
|
|
534
|
-
*
|
|
535
|
-
* @returns {string} Engine URL
|
|
536
|
-
* @private
|
|
537
|
-
*/
|
|
538
|
-
getEngineUrl() {
|
|
539
|
-
switch (this.config.environment) {
|
|
540
|
-
case 'staging':
|
|
541
|
-
return 'https://engine.staging.safepassageapp.com';
|
|
542
|
-
case 'production':
|
|
543
|
-
// Direct access to verify-engine
|
|
544
|
-
return 'https://engine.safepassageapp.com';
|
|
545
|
-
default:
|
|
546
|
-
return 'https://engine.safepassageapp.com';
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
* Get WebSocket URL based on environment
|
|
551
|
-
*
|
|
552
|
-
* Returns the appropriate WebSocket URL for the current environment.
|
|
553
|
-
* In production, WebSocket connections are proxied through portal-api.
|
|
554
|
-
*
|
|
555
|
-
* @returns {string} WebSocket URL
|
|
556
|
-
* @private
|
|
557
|
-
*/
|
|
558
|
-
getWebSocketUrl() {
|
|
559
|
-
switch (this.config.environment) {
|
|
560
|
-
case 'staging':
|
|
561
|
-
return 'wss://engine.staging.safepassageapp.com/api/websocket/stream';
|
|
562
|
-
case 'production':
|
|
563
|
-
// Direct WebSocket connection to verify-engine
|
|
564
|
-
return 'wss://engine.safepassageapp.com/api/websocket/stream';
|
|
565
|
-
default:
|
|
566
|
-
return 'wss://engine.safepassageapp.com/api/websocket/stream';
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* Detect if this is a public key (pk_ prefix) vs private key (sk_ prefix)
|
|
571
|
-
*
|
|
572
|
-
* Determines API key type based on prefix to handle different authentication flows.
|
|
573
|
-
*
|
|
574
|
-
* @returns {boolean} True if public key, false if private key
|
|
575
|
-
* @private
|
|
576
|
-
*/
|
|
577
|
-
isPublicKey() {
|
|
578
|
-
return this.config.apiKey.startsWith('pk_');
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* Create session internally for public keys
|
|
582
|
-
*
|
|
583
|
-
* Creates a verification session via the portal API for public key authentication.
|
|
584
|
-
* Generates a UUID session ID and submits session creation request with
|
|
585
|
-
* verification parameters.
|
|
586
|
-
*
|
|
587
|
-
* @param {VerificationOptions} options - Verification options
|
|
588
|
-
* @returns {Promise<string>} Created session ID
|
|
589
|
-
* @throws {Error} If session creation fails
|
|
590
|
-
* @private
|
|
591
|
-
*/
|
|
592
|
-
async createInternalSession(options) {
|
|
593
|
-
var _a, _b;
|
|
594
|
-
try {
|
|
595
|
-
const portalApiUrl = this.getPortalApiUrl();
|
|
596
|
-
const response = await fetch(`${portalApiUrl}/api/v1/sessions/create`, {
|
|
597
|
-
method: 'POST',
|
|
598
|
-
headers: {
|
|
599
|
-
'Content-Type': 'application/json',
|
|
600
|
-
Authorization: `Bearer ${this.config.apiKey}`,
|
|
601
|
-
},
|
|
602
|
-
body: JSON.stringify({
|
|
603
|
-
merchantId: this.config.apiKey, // API expects merchantId even though it uses auth header
|
|
604
|
-
returnUrl: this.config.returnUrl,
|
|
605
|
-
cancelUrl: this.config.cancelUrl,
|
|
606
|
-
challengeAge: options.challengeAge,
|
|
607
|
-
verificationMode: options.verificationMode,
|
|
608
|
-
merchantName: document.title || window.location.hostname,
|
|
609
|
-
externalUserId: options.externalUserId,
|
|
610
|
-
}),
|
|
611
|
-
});
|
|
612
|
-
if (!response.ok) {
|
|
613
|
-
const errorData = await response.json().catch(() => ({}));
|
|
614
|
-
throw new Error(`Failed to create session: ${response.status} ${response.statusText}. ${errorData.message || ''}`);
|
|
615
|
-
}
|
|
616
|
-
const sessionData = await response.json();
|
|
617
|
-
// SafePassage now generates the sessionId internally
|
|
618
|
-
const sessionId = sessionData.sessionId;
|
|
619
|
-
if (!sessionId) {
|
|
620
|
-
throw new Error('Server did not return a sessionId');
|
|
621
|
-
}
|
|
622
|
-
// Capture verifyUrl from server for redirect (ensures sessionToken is present)
|
|
623
|
-
if (sessionData.verifyUrl) {
|
|
624
|
-
this.lastVerifyUrl = sessionData.verifyUrl;
|
|
625
|
-
}
|
|
626
|
-
// Capture sessionToken for state payload (ensures UI can always auth WS)
|
|
627
|
-
if (sessionData.sessionToken) {
|
|
628
|
-
this.lastSessionToken = sessionData.sessionToken;
|
|
629
|
-
}
|
|
630
|
-
// Store the handoffToken if it exists for desktop QR flow
|
|
631
|
-
if (sessionData.handoffToken) {
|
|
632
|
-
// Store it temporarily so it can be included in the state
|
|
633
|
-
this.temporaryHandoffToken = sessionData.handoffToken;
|
|
634
|
-
}
|
|
635
|
-
// Log successful session creation
|
|
636
|
-
logSecurityEvent('INTERNAL_SESSION_CREATED', {
|
|
637
|
-
sessionId: sessionId.substring(0, 8) + '...',
|
|
638
|
-
environment: this.config.environment,
|
|
639
|
-
apiKeyType: 'public',
|
|
640
|
-
});
|
|
641
|
-
return sessionId;
|
|
642
|
-
}
|
|
643
|
-
catch (error) {
|
|
644
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
645
|
-
logSecurityEvent('INTERNAL_SESSION_FAILED', {
|
|
646
|
-
error: errorMessage,
|
|
647
|
-
environment: this.config.environment,
|
|
648
|
-
apiKeyType: 'public',
|
|
649
|
-
});
|
|
650
|
-
(_b = (_a = this.config).onError) === null || _b === void 0 ? void 0 : _b.call(_a, error);
|
|
651
|
-
throw new Error(`Failed to create verification session: ${errorMessage}`);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
// For backwards compatibility
|
|
656
|
-
export default SafePassage;
|
package/dist/index.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SafePassage SDK - Redirect-based age verification
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* ```javascript
|
|
6
|
-
* // Initialize SDK
|
|
7
|
-
* const sp = new SafePassage({
|
|
8
|
-
* apiKey: 'pk_xxxxx',
|
|
9
|
-
* returnUrl: 'https://merchant.com/verified',
|
|
10
|
-
* cancelUrl: 'https://merchant.com/cancelled'
|
|
11
|
-
* });
|
|
12
|
-
*
|
|
13
|
-
* // Trigger verification
|
|
14
|
-
* sp.verify({
|
|
15
|
-
* sessionId: generateUUID() // Merchant-generated UUID v4
|
|
16
|
-
* });
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
export { SafePassage, SafePassage as default } from './core/SafePassageSDK';
|
|
20
|
-
export type { SafePassageConfig, VerificationOptions, VerificationResult, SessionValidationResponse, } from './types';
|
|
21
|
-
export declare const VERSION = "3.4.9";
|