@safepassage/sdk 3.0.3 → 3.0.5
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 +20 -11
- package/dist/core/SafePassageSDK.d.ts +221 -0
- package/dist/core/SafePassageSDK.js +587 -0
- package/dist/index.d.ts +19 -115
- package/dist/index.js +40 -7
- package/dist/safepassage.min.js +3 -3
- package/dist/tests/SafePassageSDK.test.d.ts +4 -0
- package/dist/tests/SafePassageSDK.test.js +130 -0
- package/dist/types/index.d.ts +139 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/__mocks__/polyfills.d.ts +3 -0
- package/dist/utils/__mocks__/polyfills.js +10 -0
- package/dist/utils/crypto.d.ts +105 -0
- package/dist/utils/crypto.js +210 -0
- package/dist/utils/environment.d.ts +13 -0
- package/dist/utils/environment.js +96 -0
- package/dist/utils/polyfills.d.ts +12 -0
- package/dist/utils/polyfills.js +69 -0
- package/dist/utils/security.d.ts +50 -0
- package/dist/utils/security.js +220 -0
- package/dist/utils/validation.d.ts +21 -0
- package/dist/utils/validation.js +160 -0
- package/package.json +17 -13
- package/dist/components/SafePassageVerification.d.ts +0 -4
- package/dist/components/SafePassageVerification.js +0 -196
|
@@ -0,0 +1,587 @@
|
|
|
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
|
+
validateConfig(config);
|
|
63
|
+
this.config = {
|
|
64
|
+
...config,
|
|
65
|
+
environment: config.environment || this.detectEnvironment(),
|
|
66
|
+
mode: config.mode || 'redirect',
|
|
67
|
+
};
|
|
68
|
+
// Comprehensive environment security validation
|
|
69
|
+
validateEnvironmentSecurity(this.config.environment);
|
|
70
|
+
// Enforce HTTPS in production (additional layer)
|
|
71
|
+
enforceHTTPS(this.config.environment);
|
|
72
|
+
// Log initialization for security monitoring
|
|
73
|
+
logSecurityEvent('SDK_INITIALIZED', {
|
|
74
|
+
environment: this.config.environment,
|
|
75
|
+
mode: this.config.mode,
|
|
76
|
+
origin: window.location.origin,
|
|
77
|
+
protocol: window.location.protocol,
|
|
78
|
+
hostname: window.location.hostname,
|
|
79
|
+
});
|
|
80
|
+
// Set up automatic cleanup on page unload
|
|
81
|
+
this.setupAutoCleanup();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Initiate age verification with race condition protection
|
|
85
|
+
*
|
|
86
|
+
* Main verification method that handles session creation, security validation,
|
|
87
|
+
* and verification flow initiation. Includes race condition protection and
|
|
88
|
+
* comprehensive error handling.
|
|
89
|
+
*
|
|
90
|
+
* For public keys (pk_*), automatically creates sessions via the portal API.
|
|
91
|
+
* For private keys (sk_*), requires a pre-created sessionId.
|
|
92
|
+
*
|
|
93
|
+
* @param {VerificationOptions} [options={}] - Verification options
|
|
94
|
+
* @param {string} [options.sessionId] - Session ID (required for private keys)
|
|
95
|
+
* @param {number} [options.challengeAge] - Age challenge override
|
|
96
|
+
* @param {string} [options.verificationMode] - Verification mode override
|
|
97
|
+
* @param {string} [options.externalUserId] - External user identifier
|
|
98
|
+
* @returns {Promise<void>} Promise that resolves when verification is initiated
|
|
99
|
+
* @throws {Error} If verification cannot be started or is already in progress
|
|
100
|
+
*/
|
|
101
|
+
async verify(options = {}) {
|
|
102
|
+
const isPublicKey = this.isPublicKey();
|
|
103
|
+
let sessionId = options.sessionId;
|
|
104
|
+
// For public keys, generate session internally if not provided
|
|
105
|
+
if (isPublicKey && !sessionId) {
|
|
106
|
+
sessionId = await this.createInternalSession(options);
|
|
107
|
+
}
|
|
108
|
+
// For private keys, sessionId is required
|
|
109
|
+
if (!isPublicKey && !sessionId) {
|
|
110
|
+
throw new Error('sessionId is required for private API keys - must be a merchant-generated UUID v4');
|
|
111
|
+
}
|
|
112
|
+
if (!sessionId) {
|
|
113
|
+
throw new Error('Failed to create or obtain sessionId');
|
|
114
|
+
}
|
|
115
|
+
// Race condition check - prevent multiple simultaneous verifications
|
|
116
|
+
if (this.isVerificationInProgress) {
|
|
117
|
+
const error = new Error(`Verification already in progress for session ${this.currentSessionId?.substring(0, 8)}...`);
|
|
118
|
+
logSecurityEvent('RACE_CONDITION_PREVENTED', {
|
|
119
|
+
currentSession: this.currentSessionId?.substring(0, 8) + '...',
|
|
120
|
+
attemptedSession: options.sessionId
|
|
121
|
+
? options.sessionId.substring(0, 8) + '...'
|
|
122
|
+
: 'undefined',
|
|
123
|
+
origin: window.location.origin,
|
|
124
|
+
});
|
|
125
|
+
this.config.onError?.(error);
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
// Lock verification process
|
|
129
|
+
this.isVerificationInProgress = true;
|
|
130
|
+
this.currentSessionId = sessionId;
|
|
131
|
+
try {
|
|
132
|
+
// Rate limiting check
|
|
133
|
+
const rateLimitKey = `${this.config.apiKey}:${window.location.origin}`;
|
|
134
|
+
if (!verificationRateLimit.isAllowed(rateLimitKey)) {
|
|
135
|
+
const error = new Error('Too many verification attempts. Please wait before trying again.');
|
|
136
|
+
logSecurityEvent('RATE_LIMIT_EXCEEDED', {
|
|
137
|
+
apiKey: this.config.apiKey.substring(0, 8) + '...',
|
|
138
|
+
origin: window.location.origin,
|
|
139
|
+
sessionId: sessionId
|
|
140
|
+
? sessionId.substring(0, 8) + '...'
|
|
141
|
+
: 'undefined',
|
|
142
|
+
});
|
|
143
|
+
this.config.onError?.(error);
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
const verificationUrl = await this.buildVerificationUrl({
|
|
147
|
+
...options,
|
|
148
|
+
sessionId,
|
|
149
|
+
});
|
|
150
|
+
// Log verification attempt
|
|
151
|
+
logSecurityEvent('VERIFICATION_INITIATED', {
|
|
152
|
+
environment: this.config.environment,
|
|
153
|
+
mode: this.config.mode,
|
|
154
|
+
sessionId: sessionId ? sessionId.substring(0, 8) + '...' : 'undefined',
|
|
155
|
+
origin: window.location.origin,
|
|
156
|
+
});
|
|
157
|
+
if (this.config.mode === 'new-tab') {
|
|
158
|
+
this.openNewTab(verificationUrl, sessionId);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// For redirect mode, unlock immediately since we're leaving the page
|
|
162
|
+
this.unlockVerification();
|
|
163
|
+
this.redirect(verificationUrl);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
// Always unlock on error
|
|
168
|
+
this.unlockVerification();
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Build verification URL with HMAC-signed state
|
|
174
|
+
*
|
|
175
|
+
* Constructs the verification URL with signed state parameter containing
|
|
176
|
+
* all necessary configuration and security information. The state parameter
|
|
177
|
+
* includes HMAC signature for integrity protection.
|
|
178
|
+
*
|
|
179
|
+
* @param {VerificationOptions} options - Verification options
|
|
180
|
+
* @returns {Promise<string>} Complete verification URL
|
|
181
|
+
* @private
|
|
182
|
+
*/
|
|
183
|
+
async buildVerificationUrl(options) {
|
|
184
|
+
const baseUrl = getEnvironmentUrl(this.config.environment);
|
|
185
|
+
// Determine if we have explicit overrides
|
|
186
|
+
const hasExplicitChallengeAge = options.challengeAge !== undefined;
|
|
187
|
+
const hasExplicitVerificationMode = options.verificationMode !== undefined;
|
|
188
|
+
const hasOverrides = hasExplicitChallengeAge || hasExplicitVerificationMode;
|
|
189
|
+
const state = await generateState({
|
|
190
|
+
merchantId: this.config.apiKey,
|
|
191
|
+
sessionId: options.sessionId,
|
|
192
|
+
returnUrl: this.config.returnUrl,
|
|
193
|
+
cancelUrl: this.config.cancelUrl,
|
|
194
|
+
challengeAge: options.challengeAge || this.config.defaultChallengeAge,
|
|
195
|
+
verificationMode: options.verificationMode || this.config.defaultVerificationMode,
|
|
196
|
+
hasOverrides: hasOverrides, // Flag to indicate explicit overrides
|
|
197
|
+
externalUserId: options.externalUserId,
|
|
198
|
+
timestamp: Date.now(),
|
|
199
|
+
// Phase 2: Include config for self-contained verify-ui
|
|
200
|
+
apiUrl: this.getPortalApiUrl(),
|
|
201
|
+
engineUrl: this.getEngineUrl(),
|
|
202
|
+
wsUrl: this.getWebSocketUrl(),
|
|
203
|
+
environment: this.config.environment,
|
|
204
|
+
features: {
|
|
205
|
+
captureMode: 'enhanced_verification',
|
|
206
|
+
testMode: false,
|
|
207
|
+
warmupPeriodMs: 2000,
|
|
208
|
+
qualityThreshold: 0.6,
|
|
209
|
+
},
|
|
210
|
+
}, this.config.environment);
|
|
211
|
+
const params = new URLSearchParams({
|
|
212
|
+
state,
|
|
213
|
+
sessionId: options.sessionId,
|
|
214
|
+
mode: this.config.mode,
|
|
215
|
+
});
|
|
216
|
+
return `${baseUrl}/?${params.toString()}`;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Redirect in same tab
|
|
220
|
+
*
|
|
221
|
+
* Performs a full page redirect to the verification URL.
|
|
222
|
+
* Used for redirect mode verification.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} url - Verification URL to redirect to
|
|
225
|
+
* @private
|
|
226
|
+
*/
|
|
227
|
+
redirect(url) {
|
|
228
|
+
window.location.href = url;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Open in new tab with PostMessage communication and proper cleanup
|
|
232
|
+
*
|
|
233
|
+
* Opens verification URL in a new tab/window and sets up secure PostMessage
|
|
234
|
+
* communication for receiving verification results. Includes comprehensive
|
|
235
|
+
* security validation and automatic cleanup.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} url - Verification URL to open
|
|
238
|
+
* @param {string} sessionId - Session ID for result correlation
|
|
239
|
+
* @private
|
|
240
|
+
*/
|
|
241
|
+
openNewTab(url, sessionId) {
|
|
242
|
+
// Clean up any existing resources
|
|
243
|
+
this.cleanup();
|
|
244
|
+
// Clear any existing popup monitor interval
|
|
245
|
+
if (this.popupMonitorInterval) {
|
|
246
|
+
clearInterval(this.popupMonitorInterval);
|
|
247
|
+
this.popupMonitorInterval = null;
|
|
248
|
+
}
|
|
249
|
+
// Open new tab
|
|
250
|
+
this.popupWindow = window.open(url, 'safepassage-verify', 'width=600,height=700');
|
|
251
|
+
if (!this.popupWindow) {
|
|
252
|
+
this.config.onError?.(new Error('Failed to open verification window. Please check popup blocker settings.'));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
// Set up PostMessage listener with enhanced security
|
|
256
|
+
this.messageListener = (event) => {
|
|
257
|
+
// Enhanced origin validation with strict allowlist
|
|
258
|
+
if (!validatePostMessageOrigin(event, this.config.environment)) {
|
|
259
|
+
logSecurityEvent('POSTMESSAGE_ORIGIN_BLOCKED', {
|
|
260
|
+
origin: event.origin,
|
|
261
|
+
environment: this.config.environment,
|
|
262
|
+
expectedOrigins: `SafePassage trusted origins for ${this.config.environment}`,
|
|
263
|
+
messageType: event.data?.type,
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
// Enhanced message validation
|
|
268
|
+
const messageValidation = validateSafePassageMessage(event, sessionId);
|
|
269
|
+
if (!messageValidation.isValid) {
|
|
270
|
+
logSecurityEvent('POSTMESSAGE_VALIDATION_FAILED', {
|
|
271
|
+
error: messageValidation.error,
|
|
272
|
+
origin: event.origin,
|
|
273
|
+
sessionId: sessionId.substring(0, 8) + '...',
|
|
274
|
+
messageType: event.data?.type,
|
|
275
|
+
});
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const result = {
|
|
279
|
+
sessionId: event.data.sessionId,
|
|
280
|
+
status: event.data.status,
|
|
281
|
+
};
|
|
282
|
+
// Log successful verification completion
|
|
283
|
+
logSecurityEvent('VERIFICATION_COMPLETED', {
|
|
284
|
+
status: result.status,
|
|
285
|
+
sessionId: sessionId.substring(0, 8) + '...',
|
|
286
|
+
origin: event.origin,
|
|
287
|
+
});
|
|
288
|
+
// Clean up resources
|
|
289
|
+
this.cleanup();
|
|
290
|
+
// Unlock verification after successful completion
|
|
291
|
+
this.unlockVerification();
|
|
292
|
+
// Clear monitoring interval
|
|
293
|
+
if (this.popupMonitorInterval) {
|
|
294
|
+
clearInterval(this.popupMonitorInterval);
|
|
295
|
+
this.popupMonitorInterval = null;
|
|
296
|
+
}
|
|
297
|
+
// Trigger appropriate callback
|
|
298
|
+
if (result.status === 'verified') {
|
|
299
|
+
this.config.onComplete?.(result);
|
|
300
|
+
}
|
|
301
|
+
else if (result.status === 'cancelled') {
|
|
302
|
+
this.config.onCancel?.();
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
this.config.onError?.(new Error(`Verification failed: ${result.status}`));
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
window.addEventListener('message', this.messageListener);
|
|
309
|
+
// Monitor popup window with proper cleanup
|
|
310
|
+
this.popupMonitorInterval = setInterval(() => {
|
|
311
|
+
if (this.popupWindow && this.popupWindow.closed) {
|
|
312
|
+
// Log popup closed event
|
|
313
|
+
logSecurityEvent('POPUP_CLOSED_BY_USER', {
|
|
314
|
+
sessionId: sessionId.substring(0, 8) + '...',
|
|
315
|
+
environment: this.config.environment,
|
|
316
|
+
});
|
|
317
|
+
// Clean up and trigger cancel callback
|
|
318
|
+
this.cleanup();
|
|
319
|
+
// Unlock verification after popup closed
|
|
320
|
+
this.unlockVerification();
|
|
321
|
+
this.config.onCancel?.();
|
|
322
|
+
}
|
|
323
|
+
}, 500);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Set up automatic cleanup on page unload to prevent memory leaks
|
|
327
|
+
*
|
|
328
|
+
* Registers event listeners for page unload events to ensure proper
|
|
329
|
+
* cleanup of resources and verification state. Handles both traditional
|
|
330
|
+
* page navigation and single-page application route changes.
|
|
331
|
+
*
|
|
332
|
+
* @private
|
|
333
|
+
*/
|
|
334
|
+
setupAutoCleanup() {
|
|
335
|
+
this.unloadListener = () => {
|
|
336
|
+
// Log automatic cleanup
|
|
337
|
+
logSecurityEvent('SDK_AUTO_CLEANUP', {
|
|
338
|
+
environment: this.config.environment,
|
|
339
|
+
trigger: 'page_unload',
|
|
340
|
+
});
|
|
341
|
+
// Clean up all resources and unlock verification
|
|
342
|
+
this.cleanup();
|
|
343
|
+
this.unlockVerification();
|
|
344
|
+
};
|
|
345
|
+
// Listen for page unload events
|
|
346
|
+
window.addEventListener('beforeunload', this.unloadListener);
|
|
347
|
+
window.addEventListener('pagehide', this.unloadListener);
|
|
348
|
+
// For single-page applications, also listen for route changes
|
|
349
|
+
if (window.history && window.history.pushState) {
|
|
350
|
+
const originalPushState = window.history.pushState;
|
|
351
|
+
window.history.pushState = (...args) => {
|
|
352
|
+
this.cleanup();
|
|
353
|
+
this.unlockVerification();
|
|
354
|
+
return originalPushState.apply(window.history, args);
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Auto-detect environment based on current URL
|
|
360
|
+
*
|
|
361
|
+
* Analyzes the current hostname to determine the appropriate environment
|
|
362
|
+
* configuration. Used when environment is not explicitly specified.
|
|
363
|
+
*
|
|
364
|
+
* @returns {'production' | 'staging' | 'development'} Detected environment
|
|
365
|
+
* @private
|
|
366
|
+
*/
|
|
367
|
+
detectEnvironment() {
|
|
368
|
+
const hostname = window.location.hostname;
|
|
369
|
+
if (hostname === 'localhost' ||
|
|
370
|
+
hostname === '127.0.0.1' ||
|
|
371
|
+
hostname.includes('.local')) {
|
|
372
|
+
return 'development';
|
|
373
|
+
}
|
|
374
|
+
if (hostname.includes('staging') || hostname.includes('stage')) {
|
|
375
|
+
return 'staging';
|
|
376
|
+
}
|
|
377
|
+
return 'production';
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Unlock verification process to allow new verifications
|
|
381
|
+
*
|
|
382
|
+
* Resets the verification lock state to allow new verification attempts.
|
|
383
|
+
* Called after successful completion, errors, or cleanup.
|
|
384
|
+
*
|
|
385
|
+
* @private
|
|
386
|
+
*/
|
|
387
|
+
unlockVerification() {
|
|
388
|
+
this.isVerificationInProgress = false;
|
|
389
|
+
this.currentSessionId = null;
|
|
390
|
+
logSecurityEvent('VERIFICATION_UNLOCKED', {
|
|
391
|
+
environment: this.config.environment,
|
|
392
|
+
origin: window.location.origin,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Internal cleanup method to prevent memory leaks
|
|
397
|
+
*
|
|
398
|
+
* Cleans up popup windows, event listeners, and intervals.
|
|
399
|
+
* Does not unlock verification state - that's handled by specific callers.
|
|
400
|
+
*
|
|
401
|
+
* @private
|
|
402
|
+
*/
|
|
403
|
+
cleanup() {
|
|
404
|
+
// Close popup window
|
|
405
|
+
if (this.popupWindow && !this.popupWindow.closed) {
|
|
406
|
+
this.popupWindow.close();
|
|
407
|
+
}
|
|
408
|
+
this.popupWindow = null;
|
|
409
|
+
// Remove message listener
|
|
410
|
+
if (this.messageListener) {
|
|
411
|
+
window.removeEventListener('message', this.messageListener);
|
|
412
|
+
this.messageListener = null;
|
|
413
|
+
}
|
|
414
|
+
// Clear monitoring interval
|
|
415
|
+
if (this.popupMonitorInterval) {
|
|
416
|
+
clearInterval(this.popupMonitorInterval);
|
|
417
|
+
this.popupMonitorInterval = null;
|
|
418
|
+
}
|
|
419
|
+
// Note: Verification unlocking is handled by specific callers
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Remove auto-cleanup listeners
|
|
423
|
+
*
|
|
424
|
+
* Removes page unload event listeners that were set up for automatic cleanup.
|
|
425
|
+
*
|
|
426
|
+
* @private
|
|
427
|
+
*/
|
|
428
|
+
removeAutoCleanupListeners() {
|
|
429
|
+
if (this.unloadListener) {
|
|
430
|
+
window.removeEventListener('beforeunload', this.unloadListener);
|
|
431
|
+
window.removeEventListener('pagehide', this.unloadListener);
|
|
432
|
+
this.unloadListener = null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Public cleanup method for manual resource management
|
|
437
|
+
*
|
|
438
|
+
* Completely destroys the SDK instance, cleaning up all resources and
|
|
439
|
+
* removing all event listeners. Should be called when the SDK is no longer needed.
|
|
440
|
+
*
|
|
441
|
+
* @public
|
|
442
|
+
*/
|
|
443
|
+
destroy() {
|
|
444
|
+
// Log destruction for security monitoring
|
|
445
|
+
logSecurityEvent('SDK_DESTROYED', {
|
|
446
|
+
environment: this.config.environment,
|
|
447
|
+
origin: window.location.origin,
|
|
448
|
+
});
|
|
449
|
+
// Clean up all resources
|
|
450
|
+
this.cleanup();
|
|
451
|
+
// Unlock verification
|
|
452
|
+
this.unlockVerification();
|
|
453
|
+
// Remove auto-cleanup listeners
|
|
454
|
+
this.removeAutoCleanupListeners();
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Get Portal API URL based on environment
|
|
458
|
+
*
|
|
459
|
+
* Returns the appropriate portal-api URL for the current environment.
|
|
460
|
+
*
|
|
461
|
+
* @returns {string} Portal API URL
|
|
462
|
+
* @private
|
|
463
|
+
*/
|
|
464
|
+
getPortalApiUrl() {
|
|
465
|
+
switch (this.config.environment) {
|
|
466
|
+
case 'development':
|
|
467
|
+
return 'http://localhost:3001';
|
|
468
|
+
case 'staging':
|
|
469
|
+
case 'production':
|
|
470
|
+
return 'https://api.safepassageapp.com';
|
|
471
|
+
default:
|
|
472
|
+
return 'https://api.safepassageapp.com';
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Get Engine URL based on environment
|
|
477
|
+
*
|
|
478
|
+
* Returns the appropriate verify-engine URL for the current environment.
|
|
479
|
+
* In production, engine access is proxied through portal-api.
|
|
480
|
+
*
|
|
481
|
+
* @returns {string} Engine URL
|
|
482
|
+
* @private
|
|
483
|
+
*/
|
|
484
|
+
getEngineUrl() {
|
|
485
|
+
switch (this.config.environment) {
|
|
486
|
+
case 'development':
|
|
487
|
+
return 'http://localhost:8000';
|
|
488
|
+
case 'staging':
|
|
489
|
+
case 'production':
|
|
490
|
+
// Direct access to verify-engine
|
|
491
|
+
return 'https://engine.safepassageapp.com';
|
|
492
|
+
default:
|
|
493
|
+
return 'https://engine.safepassageapp.com';
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Get WebSocket URL based on environment
|
|
498
|
+
*
|
|
499
|
+
* Returns the appropriate WebSocket URL for the current environment.
|
|
500
|
+
* In production, WebSocket connections are proxied through portal-api.
|
|
501
|
+
*
|
|
502
|
+
* @returns {string} WebSocket URL
|
|
503
|
+
* @private
|
|
504
|
+
*/
|
|
505
|
+
getWebSocketUrl() {
|
|
506
|
+
switch (this.config.environment) {
|
|
507
|
+
case 'development':
|
|
508
|
+
return 'ws://localhost:8000/safe-passage-llm/api/v1/websocket/stream';
|
|
509
|
+
case 'staging':
|
|
510
|
+
case 'production':
|
|
511
|
+
// Direct WebSocket connection to verify-engine
|
|
512
|
+
return 'wss://engine.safepassageapp.com/safe-passage-llm/api/v1/websocket/stream';
|
|
513
|
+
default:
|
|
514
|
+
return 'wss://engine.safepassageapp.com/safe-passage-llm/api/v1/websocket/stream';
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Detect if this is a public key (pk_ prefix) vs private key (sk_ prefix)
|
|
519
|
+
*
|
|
520
|
+
* Determines API key type based on prefix to handle different authentication flows.
|
|
521
|
+
*
|
|
522
|
+
* @returns {boolean} True if public key, false if private key
|
|
523
|
+
* @private
|
|
524
|
+
*/
|
|
525
|
+
isPublicKey() {
|
|
526
|
+
return this.config.apiKey.startsWith('pk_');
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Create session internally for public keys
|
|
530
|
+
*
|
|
531
|
+
* Creates a verification session via the portal API for public key authentication.
|
|
532
|
+
* Generates a UUID session ID and submits session creation request with
|
|
533
|
+
* verification parameters.
|
|
534
|
+
*
|
|
535
|
+
* @param {VerificationOptions} options - Verification options
|
|
536
|
+
* @returns {Promise<string>} Created session ID
|
|
537
|
+
* @throws {Error} If session creation fails
|
|
538
|
+
* @private
|
|
539
|
+
*/
|
|
540
|
+
async createInternalSession(options) {
|
|
541
|
+
const sessionId = crypto.randomUUID();
|
|
542
|
+
try {
|
|
543
|
+
const portalApiUrl = this.getPortalApiUrl();
|
|
544
|
+
const response = await fetch(`${portalApiUrl}/api/v1/sessions/create`, {
|
|
545
|
+
method: 'POST',
|
|
546
|
+
headers: {
|
|
547
|
+
'Content-Type': 'application/json',
|
|
548
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
549
|
+
},
|
|
550
|
+
body: JSON.stringify({
|
|
551
|
+
merchantId: this.config.apiKey, // API expects merchantId even though it uses auth header
|
|
552
|
+
sessionId,
|
|
553
|
+
returnUrl: this.config.returnUrl,
|
|
554
|
+
cancelUrl: this.config.cancelUrl,
|
|
555
|
+
challengeAge: options.challengeAge,
|
|
556
|
+
verificationMode: options.verificationMode,
|
|
557
|
+
merchantName: document.title || window.location.hostname,
|
|
558
|
+
externalUserId: options.externalUserId,
|
|
559
|
+
}),
|
|
560
|
+
});
|
|
561
|
+
if (!response.ok) {
|
|
562
|
+
const errorData = await response.json().catch(() => ({}));
|
|
563
|
+
throw new Error(`Failed to create session: ${response.status} ${response.statusText}. ${errorData.message || ''}`);
|
|
564
|
+
}
|
|
565
|
+
await response.json();
|
|
566
|
+
// Log successful session creation
|
|
567
|
+
logSecurityEvent('INTERNAL_SESSION_CREATED', {
|
|
568
|
+
sessionId: sessionId.substring(0, 8) + '...',
|
|
569
|
+
environment: this.config.environment,
|
|
570
|
+
apiKeyType: 'public',
|
|
571
|
+
});
|
|
572
|
+
return sessionId;
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
576
|
+
logSecurityEvent('INTERNAL_SESSION_FAILED', {
|
|
577
|
+
error: errorMessage,
|
|
578
|
+
environment: this.config.environment,
|
|
579
|
+
apiKeyType: 'public',
|
|
580
|
+
});
|
|
581
|
+
this.config.onError?.(error);
|
|
582
|
+
throw new Error(`Failed to create verification session: ${errorMessage}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// For backwards compatibility
|
|
587
|
+
export default SafePassage;
|