@safepassage/sdk 3.0.3 → 3.0.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 +18 -4
- package/dist/core/SafePassageSDK.d.ts +68 -0
- package/dist/core/SafePassageSDK.js +392 -0
- package/dist/index.js +34 -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 +129 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/crypto.d.ts +31 -0
- package/dist/utils/crypto.js +128 -0
- package/dist/utils/environment.d.ts +13 -0
- package/dist/utils/environment.js +88 -0
- package/dist/utils/polyfills.d.ts +12 -0
- package/dist/utils/polyfills.js +66 -0
- package/dist/utils/security.d.ts +50 -0
- package/dist/utils/security.js +208 -0
- package/dist/utils/validation.d.ts +21 -0
- package/dist/utils/validation.js +147 -0
- package/package.json +1 -1
- package/dist/components/SafePassageVerification.d.ts +0 -4
- package/dist/components/SafePassageVerification.js +0 -196
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ safePassage.verify({
|
|
|
43
43
|
|
|
44
44
|
| Option | Type | Required | Description |
|
|
45
45
|
|--------|------|----------|-------------|
|
|
46
|
-
| apiKey | string | Yes | Your API key (
|
|
46
|
+
| apiKey | string | Yes | Your API key (pk_xxx for client-side, sk_xxx for server-side) |
|
|
47
47
|
| returnUrl | string | Yes | URL to redirect after successful verification |
|
|
48
48
|
| cancelUrl | string | Yes | URL to redirect if user cancels |
|
|
49
49
|
| environment | string | No | 'production', 'staging', or 'development' (auto-detected) |
|
|
@@ -52,13 +52,27 @@ safePassage.verify({
|
|
|
52
52
|
| onCancel | function | No | Callback for new-tab mode cancellation |
|
|
53
53
|
| onError | function | No | Error handler |
|
|
54
54
|
|
|
55
|
+
### API Key Types
|
|
56
|
+
|
|
57
|
+
SafePassage provides two types of API keys:
|
|
58
|
+
|
|
59
|
+
- **Public Keys (`pk_`)**: Safe for client-side use (websites, mobile apps)
|
|
60
|
+
- Limited to creating and initiating verifications
|
|
61
|
+
- Cannot read verification results or override settings
|
|
62
|
+
- SDK auto-generates sessionId if not provided
|
|
63
|
+
|
|
64
|
+
- **Secret Keys (`sk_`)**: Server-side only - keep these private!
|
|
65
|
+
- Full API access including reading verification results
|
|
66
|
+
- Can override challenge age and verification mode
|
|
67
|
+
- Requires merchant-generated sessionId
|
|
68
|
+
|
|
55
69
|
## Verification Options
|
|
56
70
|
|
|
57
71
|
```javascript
|
|
58
72
|
safePassage.verify({
|
|
59
|
-
sessionId: 'uuid-v4', // Required
|
|
60
|
-
challengeAge: 30, // Optional: min 25 (
|
|
61
|
-
verificationMode: 'L2' // Optional: 'L1' or 'L2' (
|
|
73
|
+
sessionId: 'uuid-v4', // Required for sk_ keys, optional for pk_ keys
|
|
74
|
+
challengeAge: 30, // Optional: min 25 (sk_ keys only)
|
|
75
|
+
verificationMode: 'L2' // Optional: 'L1' or 'L2' (sk_ keys only)
|
|
62
76
|
});
|
|
63
77
|
```
|
|
64
78
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SafePassage SDK - Redirect-based age verification
|
|
3
|
+
* Lightweight SDK for integrating SafePassage age verification using redirect flow
|
|
4
|
+
*/
|
|
5
|
+
import type { SafePassageConfig, VerificationOptions } from '../types';
|
|
6
|
+
export declare class SafePassage {
|
|
7
|
+
private config;
|
|
8
|
+
private popupWindow;
|
|
9
|
+
private messageListener;
|
|
10
|
+
private popupMonitorInterval;
|
|
11
|
+
private unloadListener;
|
|
12
|
+
private isVerificationInProgress;
|
|
13
|
+
private currentSessionId;
|
|
14
|
+
constructor(config: SafePassageConfig);
|
|
15
|
+
/**
|
|
16
|
+
* Initiate age verification with race condition protection
|
|
17
|
+
*/
|
|
18
|
+
verify(options?: VerificationOptions): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Build verification URL with HMAC-signed state
|
|
21
|
+
*/
|
|
22
|
+
private buildVerificationUrl;
|
|
23
|
+
/**
|
|
24
|
+
* Redirect in same tab
|
|
25
|
+
*/
|
|
26
|
+
private redirect;
|
|
27
|
+
/**
|
|
28
|
+
* Open in new tab with PostMessage communication and proper cleanup
|
|
29
|
+
*/
|
|
30
|
+
private openNewTab;
|
|
31
|
+
/**
|
|
32
|
+
* Set up automatic cleanup on page unload to prevent memory leaks
|
|
33
|
+
*/
|
|
34
|
+
private setupAutoCleanup;
|
|
35
|
+
/**
|
|
36
|
+
* Auto-detect environment based on current URL
|
|
37
|
+
*/
|
|
38
|
+
private detectEnvironment;
|
|
39
|
+
/**
|
|
40
|
+
* Unlock verification process to allow new verifications
|
|
41
|
+
*/
|
|
42
|
+
private unlockVerification;
|
|
43
|
+
/**
|
|
44
|
+
* Internal cleanup method to prevent memory leaks
|
|
45
|
+
*/
|
|
46
|
+
private cleanup;
|
|
47
|
+
/**
|
|
48
|
+
* Remove auto-cleanup listeners
|
|
49
|
+
*/
|
|
50
|
+
private removeAutoCleanupListeners;
|
|
51
|
+
/**
|
|
52
|
+
* Public cleanup method for manual resource management
|
|
53
|
+
*/
|
|
54
|
+
destroy(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Detect if this is a public key (pk_ prefix) vs private key (sk_ prefix)
|
|
57
|
+
*/
|
|
58
|
+
private isPublicKey;
|
|
59
|
+
/**
|
|
60
|
+
* Create session internally for public keys
|
|
61
|
+
*/
|
|
62
|
+
private createInternalSession;
|
|
63
|
+
/**
|
|
64
|
+
* Get portal API URL based on environment
|
|
65
|
+
*/
|
|
66
|
+
private getPortalApiUrl;
|
|
67
|
+
}
|
|
68
|
+
export default SafePassage;
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SafePassage SDK - Redirect-based age verification
|
|
3
|
+
* Lightweight SDK for integrating SafePassage age verification using redirect flow
|
|
4
|
+
*/
|
|
5
|
+
import { generateState, validateConfig } from '../utils/validation';
|
|
6
|
+
import { getEnvironmentUrl, validateEnvironmentSecurity } from '../utils/environment';
|
|
7
|
+
import { validatePostMessageOrigin, validateSafePassageMessage, enforceHTTPS, verificationRateLimit, logSecurityEvent } from '../utils/security';
|
|
8
|
+
export class SafePassage {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.popupWindow = null;
|
|
11
|
+
this.messageListener = null;
|
|
12
|
+
this.popupMonitorInterval = null;
|
|
13
|
+
this.unloadListener = null;
|
|
14
|
+
this.isVerificationInProgress = false;
|
|
15
|
+
this.currentSessionId = null;
|
|
16
|
+
validateConfig(config);
|
|
17
|
+
this.config = {
|
|
18
|
+
...config,
|
|
19
|
+
environment: config.environment || this.detectEnvironment(),
|
|
20
|
+
mode: config.mode || 'redirect'
|
|
21
|
+
};
|
|
22
|
+
// Comprehensive environment security validation
|
|
23
|
+
validateEnvironmentSecurity(this.config.environment);
|
|
24
|
+
// Enforce HTTPS in production (additional layer)
|
|
25
|
+
enforceHTTPS(this.config.environment);
|
|
26
|
+
// Log initialization for security monitoring
|
|
27
|
+
logSecurityEvent('SDK_INITIALIZED', {
|
|
28
|
+
environment: this.config.environment,
|
|
29
|
+
mode: this.config.mode,
|
|
30
|
+
origin: window.location.origin,
|
|
31
|
+
protocol: window.location.protocol,
|
|
32
|
+
hostname: window.location.hostname
|
|
33
|
+
});
|
|
34
|
+
// Set up automatic cleanup on page unload
|
|
35
|
+
this.setupAutoCleanup();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Initiate age verification with race condition protection
|
|
39
|
+
*/
|
|
40
|
+
async verify(options = {}) {
|
|
41
|
+
const isPublicKey = this.isPublicKey();
|
|
42
|
+
let sessionId = options.sessionId;
|
|
43
|
+
// For public keys, generate session internally if not provided
|
|
44
|
+
if (isPublicKey && !sessionId) {
|
|
45
|
+
sessionId = await this.createInternalSession(options);
|
|
46
|
+
}
|
|
47
|
+
// For private keys, sessionId is required
|
|
48
|
+
if (!isPublicKey && !sessionId) {
|
|
49
|
+
throw new Error('sessionId is required for private API keys - must be a merchant-generated UUID v4');
|
|
50
|
+
}
|
|
51
|
+
if (!sessionId) {
|
|
52
|
+
throw new Error('Failed to create or obtain sessionId');
|
|
53
|
+
}
|
|
54
|
+
// Race condition check - prevent multiple simultaneous verifications
|
|
55
|
+
if (this.isVerificationInProgress) {
|
|
56
|
+
const error = new Error(`Verification already in progress for session ${this.currentSessionId?.substring(0, 8)}...`);
|
|
57
|
+
logSecurityEvent('RACE_CONDITION_PREVENTED', {
|
|
58
|
+
currentSession: this.currentSessionId?.substring(0, 8) + '...',
|
|
59
|
+
attemptedSession: options.sessionId ? options.sessionId.substring(0, 8) + '...' : 'undefined',
|
|
60
|
+
origin: window.location.origin
|
|
61
|
+
});
|
|
62
|
+
this.config.onError?.(error);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
// Lock verification process
|
|
66
|
+
this.isVerificationInProgress = true;
|
|
67
|
+
this.currentSessionId = sessionId;
|
|
68
|
+
try {
|
|
69
|
+
// Rate limiting check
|
|
70
|
+
const rateLimitKey = `${this.config.apiKey}:${window.location.origin}`;
|
|
71
|
+
if (!verificationRateLimit.isAllowed(rateLimitKey)) {
|
|
72
|
+
const error = new Error('Too many verification attempts. Please wait before trying again.');
|
|
73
|
+
logSecurityEvent('RATE_LIMIT_EXCEEDED', {
|
|
74
|
+
apiKey: this.config.apiKey.substring(0, 8) + '...',
|
|
75
|
+
origin: window.location.origin,
|
|
76
|
+
sessionId: sessionId ? sessionId.substring(0, 8) + '...' : 'undefined'
|
|
77
|
+
});
|
|
78
|
+
this.config.onError?.(error);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
const verificationUrl = await this.buildVerificationUrl({ ...options, sessionId });
|
|
82
|
+
// Log verification attempt
|
|
83
|
+
logSecurityEvent('VERIFICATION_INITIATED', {
|
|
84
|
+
environment: this.config.environment,
|
|
85
|
+
mode: this.config.mode,
|
|
86
|
+
sessionId: sessionId ? sessionId.substring(0, 8) + '...' : 'undefined',
|
|
87
|
+
origin: window.location.origin
|
|
88
|
+
});
|
|
89
|
+
if (this.config.mode === 'new-tab') {
|
|
90
|
+
this.openNewTab(verificationUrl, sessionId);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// For redirect mode, unlock immediately since we're leaving the page
|
|
94
|
+
this.unlockVerification();
|
|
95
|
+
this.redirect(verificationUrl);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
// Always unlock on error
|
|
100
|
+
this.unlockVerification();
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Build verification URL with HMAC-signed state
|
|
106
|
+
*/
|
|
107
|
+
async buildVerificationUrl(options) {
|
|
108
|
+
const baseUrl = getEnvironmentUrl(this.config.environment);
|
|
109
|
+
// Determine if we have explicit overrides
|
|
110
|
+
const hasExplicitChallengeAge = options.challengeAge !== undefined;
|
|
111
|
+
const hasExplicitVerificationMode = options.verificationMode !== undefined;
|
|
112
|
+
const hasOverrides = hasExplicitChallengeAge || hasExplicitVerificationMode;
|
|
113
|
+
const state = await generateState({
|
|
114
|
+
merchantId: this.config.apiKey,
|
|
115
|
+
sessionId: options.sessionId,
|
|
116
|
+
returnUrl: this.config.returnUrl,
|
|
117
|
+
cancelUrl: this.config.cancelUrl,
|
|
118
|
+
challengeAge: options.challengeAge || this.config.defaultChallengeAge,
|
|
119
|
+
verificationMode: options.verificationMode || this.config.defaultVerificationMode,
|
|
120
|
+
hasOverrides: hasOverrides, // Flag to indicate explicit overrides
|
|
121
|
+
externalUserId: options.externalUserId,
|
|
122
|
+
timestamp: Date.now()
|
|
123
|
+
}, this.config.environment);
|
|
124
|
+
const params = new URLSearchParams({
|
|
125
|
+
state,
|
|
126
|
+
sessionId: options.sessionId,
|
|
127
|
+
mode: this.config.mode
|
|
128
|
+
});
|
|
129
|
+
return `${baseUrl}/verify?${params.toString()}`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Redirect in same tab
|
|
133
|
+
*/
|
|
134
|
+
redirect(url) {
|
|
135
|
+
window.location.href = url;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Open in new tab with PostMessage communication and proper cleanup
|
|
139
|
+
*/
|
|
140
|
+
openNewTab(url, sessionId) {
|
|
141
|
+
// Clean up any existing resources
|
|
142
|
+
this.cleanup();
|
|
143
|
+
// Clear any existing popup monitor interval
|
|
144
|
+
if (this.popupMonitorInterval) {
|
|
145
|
+
clearInterval(this.popupMonitorInterval);
|
|
146
|
+
this.popupMonitorInterval = null;
|
|
147
|
+
}
|
|
148
|
+
// Open new tab
|
|
149
|
+
this.popupWindow = window.open(url, 'safepassage-verify', 'width=600,height=700');
|
|
150
|
+
if (!this.popupWindow) {
|
|
151
|
+
this.config.onError?.(new Error('Failed to open verification window. Please check popup blocker settings.'));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Set up PostMessage listener with enhanced security
|
|
155
|
+
this.messageListener = (event) => {
|
|
156
|
+
// Enhanced origin validation with strict allowlist
|
|
157
|
+
if (!validatePostMessageOrigin(event, this.config.environment)) {
|
|
158
|
+
logSecurityEvent('POSTMESSAGE_ORIGIN_BLOCKED', {
|
|
159
|
+
origin: event.origin,
|
|
160
|
+
environment: this.config.environment,
|
|
161
|
+
expectedOrigins: `SafePassage trusted origins for ${this.config.environment}`,
|
|
162
|
+
messageType: event.data?.type
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Enhanced message validation
|
|
167
|
+
const messageValidation = validateSafePassageMessage(event, sessionId);
|
|
168
|
+
if (!messageValidation.isValid) {
|
|
169
|
+
logSecurityEvent('POSTMESSAGE_VALIDATION_FAILED', {
|
|
170
|
+
error: messageValidation.error,
|
|
171
|
+
origin: event.origin,
|
|
172
|
+
sessionId: sessionId.substring(0, 8) + '...',
|
|
173
|
+
messageType: event.data?.type
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const result = {
|
|
178
|
+
sessionId: event.data.sessionId,
|
|
179
|
+
status: event.data.status
|
|
180
|
+
};
|
|
181
|
+
// Log successful verification completion
|
|
182
|
+
logSecurityEvent('VERIFICATION_COMPLETED', {
|
|
183
|
+
status: result.status,
|
|
184
|
+
sessionId: sessionId.substring(0, 8) + '...',
|
|
185
|
+
origin: event.origin
|
|
186
|
+
});
|
|
187
|
+
// Clean up resources
|
|
188
|
+
this.cleanup();
|
|
189
|
+
// Unlock verification after successful completion
|
|
190
|
+
this.unlockVerification();
|
|
191
|
+
// Clear monitoring interval
|
|
192
|
+
if (this.popupMonitorInterval) {
|
|
193
|
+
clearInterval(this.popupMonitorInterval);
|
|
194
|
+
this.popupMonitorInterval = null;
|
|
195
|
+
}
|
|
196
|
+
// Trigger appropriate callback
|
|
197
|
+
if (result.status === 'verified') {
|
|
198
|
+
this.config.onComplete?.(result);
|
|
199
|
+
}
|
|
200
|
+
else if (result.status === 'cancelled') {
|
|
201
|
+
this.config.onCancel?.();
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
this.config.onError?.(new Error(`Verification failed: ${result.status}`));
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
window.addEventListener('message', this.messageListener);
|
|
208
|
+
// Monitor popup window with proper cleanup
|
|
209
|
+
this.popupMonitorInterval = setInterval(() => {
|
|
210
|
+
if (this.popupWindow && this.popupWindow.closed) {
|
|
211
|
+
// Log popup closed event
|
|
212
|
+
logSecurityEvent('POPUP_CLOSED_BY_USER', {
|
|
213
|
+
sessionId: sessionId.substring(0, 8) + '...',
|
|
214
|
+
environment: this.config.environment
|
|
215
|
+
});
|
|
216
|
+
// Clean up and trigger cancel callback
|
|
217
|
+
this.cleanup();
|
|
218
|
+
// Unlock verification after popup closed
|
|
219
|
+
this.unlockVerification();
|
|
220
|
+
this.config.onCancel?.();
|
|
221
|
+
}
|
|
222
|
+
}, 500);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Set up automatic cleanup on page unload to prevent memory leaks
|
|
226
|
+
*/
|
|
227
|
+
setupAutoCleanup() {
|
|
228
|
+
this.unloadListener = () => {
|
|
229
|
+
// Log automatic cleanup
|
|
230
|
+
logSecurityEvent('SDK_AUTO_CLEANUP', {
|
|
231
|
+
environment: this.config.environment,
|
|
232
|
+
trigger: 'page_unload'
|
|
233
|
+
});
|
|
234
|
+
// Clean up all resources and unlock verification
|
|
235
|
+
this.cleanup();
|
|
236
|
+
this.unlockVerification();
|
|
237
|
+
};
|
|
238
|
+
// Listen for page unload events
|
|
239
|
+
window.addEventListener('beforeunload', this.unloadListener);
|
|
240
|
+
window.addEventListener('pagehide', this.unloadListener);
|
|
241
|
+
// For single-page applications, also listen for route changes
|
|
242
|
+
if (window.history && window.history.pushState) {
|
|
243
|
+
const originalPushState = window.history.pushState;
|
|
244
|
+
window.history.pushState = (...args) => {
|
|
245
|
+
this.cleanup();
|
|
246
|
+
this.unlockVerification();
|
|
247
|
+
return originalPushState.apply(window.history, args);
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Auto-detect environment based on current URL
|
|
253
|
+
*/
|
|
254
|
+
detectEnvironment() {
|
|
255
|
+
const hostname = window.location.hostname;
|
|
256
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.includes('.local')) {
|
|
257
|
+
return 'development';
|
|
258
|
+
}
|
|
259
|
+
if (hostname.includes('staging') || hostname.includes('stage')) {
|
|
260
|
+
return 'staging';
|
|
261
|
+
}
|
|
262
|
+
return 'production';
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Unlock verification process to allow new verifications
|
|
266
|
+
*/
|
|
267
|
+
unlockVerification() {
|
|
268
|
+
this.isVerificationInProgress = false;
|
|
269
|
+
this.currentSessionId = null;
|
|
270
|
+
logSecurityEvent('VERIFICATION_UNLOCKED', {
|
|
271
|
+
environment: this.config.environment,
|
|
272
|
+
origin: window.location.origin
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Internal cleanup method to prevent memory leaks
|
|
277
|
+
*/
|
|
278
|
+
cleanup() {
|
|
279
|
+
// Close popup window
|
|
280
|
+
if (this.popupWindow && !this.popupWindow.closed) {
|
|
281
|
+
this.popupWindow.close();
|
|
282
|
+
}
|
|
283
|
+
this.popupWindow = null;
|
|
284
|
+
// Remove message listener
|
|
285
|
+
if (this.messageListener) {
|
|
286
|
+
window.removeEventListener('message', this.messageListener);
|
|
287
|
+
this.messageListener = null;
|
|
288
|
+
}
|
|
289
|
+
// Clear monitoring interval
|
|
290
|
+
if (this.popupMonitorInterval) {
|
|
291
|
+
clearInterval(this.popupMonitorInterval);
|
|
292
|
+
this.popupMonitorInterval = null;
|
|
293
|
+
}
|
|
294
|
+
// Note: Verification unlocking is handled by specific callers
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Remove auto-cleanup listeners
|
|
298
|
+
*/
|
|
299
|
+
removeAutoCleanupListeners() {
|
|
300
|
+
if (this.unloadListener) {
|
|
301
|
+
window.removeEventListener('beforeunload', this.unloadListener);
|
|
302
|
+
window.removeEventListener('pagehide', this.unloadListener);
|
|
303
|
+
this.unloadListener = null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Public cleanup method for manual resource management
|
|
308
|
+
*/
|
|
309
|
+
destroy() {
|
|
310
|
+
// Log destruction for security monitoring
|
|
311
|
+
logSecurityEvent('SDK_DESTROYED', {
|
|
312
|
+
environment: this.config.environment,
|
|
313
|
+
origin: window.location.origin
|
|
314
|
+
});
|
|
315
|
+
// Clean up all resources
|
|
316
|
+
this.cleanup();
|
|
317
|
+
// Unlock verification
|
|
318
|
+
this.unlockVerification();
|
|
319
|
+
// Remove auto-cleanup listeners
|
|
320
|
+
this.removeAutoCleanupListeners();
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Detect if this is a public key (pk_ prefix) vs private key (sk_ prefix)
|
|
324
|
+
*/
|
|
325
|
+
isPublicKey() {
|
|
326
|
+
return this.config.apiKey.startsWith('pk_');
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Create session internally for public keys
|
|
330
|
+
*/
|
|
331
|
+
async createInternalSession(options) {
|
|
332
|
+
const sessionId = crypto.randomUUID();
|
|
333
|
+
try {
|
|
334
|
+
const portalApiUrl = this.getPortalApiUrl();
|
|
335
|
+
const response = await fetch(`${portalApiUrl}/api/v1/sessions/create`, {
|
|
336
|
+
method: 'POST',
|
|
337
|
+
headers: {
|
|
338
|
+
'Content-Type': 'application/json',
|
|
339
|
+
'Authorization': `Bearer ${this.config.apiKey}`
|
|
340
|
+
},
|
|
341
|
+
body: JSON.stringify({
|
|
342
|
+
merchantId: this.config.apiKey, // API expects merchantId even though it uses auth header
|
|
343
|
+
sessionId,
|
|
344
|
+
returnUrl: this.config.returnUrl,
|
|
345
|
+
cancelUrl: this.config.cancelUrl,
|
|
346
|
+
challengeAge: options.challengeAge,
|
|
347
|
+
verificationMode: options.verificationMode,
|
|
348
|
+
merchantName: document.title || window.location.hostname,
|
|
349
|
+
externalUserId: options.externalUserId
|
|
350
|
+
})
|
|
351
|
+
});
|
|
352
|
+
if (!response.ok) {
|
|
353
|
+
const errorData = await response.json().catch(() => ({}));
|
|
354
|
+
throw new Error(`Failed to create session: ${response.status} ${response.statusText}. ${errorData.message || ''}`);
|
|
355
|
+
}
|
|
356
|
+
const sessionData = await response.json();
|
|
357
|
+
// Log successful session creation
|
|
358
|
+
logSecurityEvent('INTERNAL_SESSION_CREATED', {
|
|
359
|
+
sessionId: sessionId.substring(0, 8) + '...',
|
|
360
|
+
environment: this.config.environment,
|
|
361
|
+
apiKeyType: 'public'
|
|
362
|
+
});
|
|
363
|
+
return sessionId;
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
367
|
+
logSecurityEvent('INTERNAL_SESSION_FAILED', {
|
|
368
|
+
error: errorMessage,
|
|
369
|
+
environment: this.config.environment,
|
|
370
|
+
apiKeyType: 'public'
|
|
371
|
+
});
|
|
372
|
+
this.config.onError?.(error);
|
|
373
|
+
throw new Error(`Failed to create verification session: ${errorMessage}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Get portal API URL based on environment
|
|
378
|
+
*/
|
|
379
|
+
getPortalApiUrl() {
|
|
380
|
+
switch (this.config.environment) {
|
|
381
|
+
case 'production':
|
|
382
|
+
return 'https://api.safepassageapp.com';
|
|
383
|
+
case 'staging':
|
|
384
|
+
return 'https://api-staging.safepassageapp.com';
|
|
385
|
+
case 'development':
|
|
386
|
+
default:
|
|
387
|
+
return 'http://localhost:3001';
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// For backwards compatibility
|
|
392
|
+
export default SafePassage;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
|
|
1
|
+
/**
|
|
2
|
+
* SafePassage SDK - Redirect-based age verification
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```javascript
|
|
6
|
+
* // Initialize SDK
|
|
7
|
+
* const sp = new SafePassage({
|
|
8
|
+
* apiKey: 'pk_live_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
|
+
// 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 - Updated to reflect security improvements
|
|
28
|
+
export const VERSION = '3.0.4';
|
|
29
|
+
// For UMD builds
|
|
30
|
+
if (typeof window !== 'undefined' && window) {
|
|
31
|
+
const { SafePassage } = require('./core/SafePassageSDK');
|
|
32
|
+
window.SafePassage = SafePassage;
|
|
33
|
+
window.SafePassage.VERSION = VERSION;
|
|
34
|
+
}
|
package/dist/safepassage.min.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/* SafePassage SDK v3.0.
|
|
2
|
-
"use strict";var SafePassageSDK=(()=>{var d=Object.defineProperty;var K=Object.getOwnPropertyDescriptor;var W=Object.getOwnPropertyNames;var H=Object.prototype.hasOwnProperty;var p=(t,e)=>()=>(t&&(e=t(t=0)),e);var u=(t,e)=>{for(var i in e)d(t,i,{get:e[i],enumerable:!0})},q=(t,e,i,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of W(e))!H.call(t,o)&&o!==i&&d(t,o,{get:()=>e[o],enumerable:!(n=K(e,o))||n.enumerable});return t};var I=t=>q(d({},"__esModule",{value:!0}),t);function F(t,e){return E[e].includes(t)}function P(t,e,i=[]){let{origin:n}=t;return F(n,e)||i.length>0&&i.some(r=>{if(r.startsWith("*.")){let s=r.slice(2);return n.endsWith(`.${s}`)||n===`https://${s}`||n===`http://${s}`}return n===r})?!0:(console.warn(`SafePassage Security: Blocked PostMessage from untrusted origin: ${n}`,{environment:e,trustedOrigins:E[e],allowedCustomOrigins:i,eventType:t.data?.type}),!1)}function T(t,e){let{data:i}=t;return!i||typeof i!="object"?{isValid:!1,error:"Invalid message format"}:i.type!=="safepassage:verification:complete"?{isValid:!1,error:"Invalid message type"}:!i.sessionId||i.sessionId!==e?{isValid:!1,error:"Session ID mismatch"}:!i.status||!["verified","failed","cancelled"].includes(i.status)?{isValid:!1,error:"Invalid status value"}:{isValid:!0}}function U(t){if(t==="production"&&window.location.protocol!=="https:"){let e=window.location.href.replace("http:","https:");console.error("SafePassage Security: HTTPS required in production. Redirecting...",{current:window.location.href,redirect:e}),window.location.replace(e)}}function f(t,e){try{let i=new URL(t);if(e==="production"&&i.protocol!=="https:")return{isValid:!1,error:"HTTPS required for return URLs in production"};if(e==="development"&&!(i.hostname==="localhost"||i.hostname==="127.0.0.1"||i.hostname.endsWith(".local"))&&i.protocol!=="https:")return{isValid:!1,error:"Non-localhost URLs must use HTTPS"};if(e==="staging"&&i.protocol!=="https:")return{isValid:!1,error:"HTTPS required for return URLs in staging"};let n=[/data:/i,/javascript:/i,/vbscript:/i,/file:/i,/ftp:/i];for(let o of n)if(o.test(t))return{isValid:!1,error:"Blocked suspicious URL scheme"};return{isValid:!0}}catch{return{isValid:!1,error:"Invalid URL format"}}}function a(t,e){console.warn(`SafePassage Security Event: ${t}`,{timestamp:new Date().toISOString(),userAgent:navigator.userAgent,url:window.location.href,...e})}var E,g,A,h=p(()=>{"use strict";E={production:["https://verify.safepassageapp.com","https://portal.safepassageapp.com","https://api.safepassageapp.com"],staging:["https://verify-staging.safepassageapp.com","https://portal-staging.safepassageapp.com","https://api-staging.safepassageapp.com"],development:["http://localhost:5173","http://localhost:3000","http://localhost:3001","http://localhost:3002","http://127.0.0.1:5173","http://127.0.0.1:3000","http://127.0.0.1:3001","http://127.0.0.1:3002"]};g=class{constructor(){this.attempts=new Map;this.maxAttempts=5;this.timeWindow=6e4}isAllowed(e){let i=Date.now(),o=(this.attempts.get(e)||[]).filter(r=>i-r<this.timeWindow);return o.length>=this.maxAttempts?(console.warn(`SafePassage Security: Rate limit exceeded for ${e}`),!1):(o.push(i),this.attempts.set(e,o),!0)}reset(e){this.attempts.delete(e)}},A=new g});var L={};u(L,{createSignedState:()=>J,generateHMAC:()=>m,generateSecureToken:()=>V,getSigningSecret:()=>w,parseSignedState:()=>B,verifyHMAC:()=>b});async function m(t,e){let i=new TextEncoder,n=i.encode(e),o=i.encode(t),r=await crypto.subtle.importKey("raw",n,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),s=await crypto.subtle.sign("HMAC",r,o);return Array.from(new Uint8Array(s)).map(l=>l.toString(16).padStart(2,"0")).join("")}async function b(t,e,i){try{let n=await m(t,i);return j(e,n)}catch{return!1}}function j(t,e){if(t.length!==e.length)return!1;let i=0;for(let n=0;n<t.length;n++)i|=t.charCodeAt(n)^e.charCodeAt(n);return i===0}function V(t=32){let e=new Uint8Array(t);return crypto.getRandomValues(e),Array.from(e,i=>i.toString(16).padStart(2,"0")).join("")}function w(t){return{production:"safepassage-prod-hmac-2025",staging:"safepassage-stage-hmac-2025",development:"safepassage-dev-hmac-2025"}[t]}async function J(t,e){let i={...t,timestamp:Date.now(),nonce:V(16)},n=JSON.stringify(i),o=w(e),r=await m(n,o);return btoa(JSON.stringify({data:i,signature:r}))}async function B(t,e,i=10*60*1e3){try{let n=atob(t),o=JSON.parse(n);if(!o.data||!o.signature)return console.warn("SafePassage: Invalid signed state format"),null;let{data:r,signature:s}=o,l=JSON.stringify(r),$=w(e);if(!await b(l,s,$))return console.warn("SafePassage: State signature verification failed"),null;if(r.timestamp){let y=Date.now()-r.timestamp;if(y>i)return console.warn("SafePassage: State parameter expired",{age:y,maxAge:i}),null}let{timestamp:ie,nonce:ne,...k}=r;return k}catch(n){return console.warn("SafePassage: Failed to parse signed state",n),null}}var x=p(()=>{"use strict"});function M(t){if(!t.apiKey)throw new Error("apiKey is required");if(!G.test(t.apiKey))throw new Error("Invalid apiKey format. Expected pk_xxx (public) or sk_xxx (private)");if(!t.returnUrl)throw new Error("returnUrl is required");if(!t.cancelUrl)throw new Error("cancelUrl is required");let e=Y(),i=f(t.returnUrl,e);if(!i.isValid)throw new Error(`returnUrl validation failed: ${i.error}`);let n=f(t.cancelUrl,e);if(!n.isValid)throw new Error(`cancelUrl validation failed: ${n.error}`);if(t.defaultChallengeAge!==void 0&&t.defaultChallengeAge<C)throw new Error(`defaultChallengeAge must be at least ${C}`);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 Y(){let t=window.location.hostname;return t==="localhost"||t==="127.0.0.1"||t.includes(".local")?"development":t.includes("staging")||t.includes("stage")?"staging":"production"}async function R(t,e){let{createSignedState:i}=await Promise.resolve().then(()=>(x(),L));return i(t,e)}var C,G,O=p(()=>{"use strict";h();C=25,G=/^(pk_|sk_)[a-zA-Z0-9]+$/});function v(t){let e=z[t];if((t==="production"||t==="staging")&&!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function Z(t){let i={production:"https://api.safepassageapp.com",staging:"https://api-staging.safepassageapp.com",development:"http://localhost:3001"}[t];if((t==="production"||t==="staging")&&!i.startsWith("https://"))throw new Error(`HTTPS required for API URLs in ${t} environment`);return i}function D(t){let e=window.location.protocol==="https:",i=window.location.hostname;switch(t){case"production":if(!e)throw new Error("SafePassage requires HTTPS in production environment");break;case"staging":e||console.warn("SafePassage Warning: HTTPS strongly recommended in staging environment");break;case"development":let n=i==="localhost"||i==="127.0.0.1"||i.includes(".local");!e&&!n&&console.warn("SafePassage Warning: HTTPS recommended for non-localhost development");break}try{v(t),Z(t)}catch(n){throw new Error(`Environment configuration validation failed: ${n}`)}}var z,N=p(()=>{"use strict";z={production:"https://verify.safepassageapp.com",staging:"https://verify-staging.safepassageapp.com",development:"http://localhost:5173"}});var _={};u(_,{SafePassage:()=>c,default:()=>X});var c,X,S=p(()=>{"use strict";O();N();h();c=class{constructor(e){this.popupWindow=null;this.messageListener=null;this.popupMonitorInterval=null;this.unloadListener=null;this.isVerificationInProgress=!1;this.currentSessionId=null;M(e),this.config={...e,environment:e.environment||this.detectEnvironment(),mode:e.mode||"redirect"},D(this.config.environment),U(this.config.environment),a("SDK_INITIALIZED",{environment:this.config.environment,mode:this.config.mode,origin:window.location.origin,protocol:window.location.protocol,hostname:window.location.hostname}),this.setupAutoCleanup()}async verify(e={}){let i=this.isPublicKey(),n=e.sessionId;if(i&&!n&&(n=await this.createInternalSession(e)),!i&&!n)throw new Error("sessionId is required for private API keys - must be a merchant-generated UUID v4");if(!n)throw new Error("Failed to create or obtain sessionId");if(this.isVerificationInProgress){let o=new Error(`Verification already in progress for session ${this.currentSessionId?.substring(0,8)}...`);throw a("RACE_CONDITION_PREVENTED",{currentSession:this.currentSessionId?.substring(0,8)+"...",attemptedSession:e.sessionId?e.sessionId.substring(0,8)+"...":"undefined",origin:window.location.origin}),this.config.onError?.(o),o}this.isVerificationInProgress=!0,this.currentSessionId=n;try{let o=`${this.config.apiKey}:${window.location.origin}`;if(!A.isAllowed(o)){let s=new Error("Too many verification attempts. Please wait before trying again.");throw a("RATE_LIMIT_EXCEEDED",{apiKey:this.config.apiKey.substring(0,8)+"...",origin:window.location.origin,sessionId:n?n.substring(0,8)+"...":"undefined"}),this.config.onError?.(s),s}let r=await this.buildVerificationUrl({...e,sessionId:n});a("VERIFICATION_INITIATED",{environment:this.config.environment,mode:this.config.mode,sessionId:n?n.substring(0,8)+"...":"undefined",origin:window.location.origin}),this.config.mode==="new-tab"?this.openNewTab(r,n):(this.unlockVerification(),this.redirect(r))}catch(o){throw this.unlockVerification(),o}}async buildVerificationUrl(e){let i=v(this.config.environment),n=e.challengeAge!==void 0,o=e.verificationMode!==void 0,r=n||o,s=await R({merchantId:this.config.apiKey,sessionId:e.sessionId,returnUrl:this.config.returnUrl,cancelUrl:this.config.cancelUrl,challengeAge:e.challengeAge||this.config.defaultChallengeAge,verificationMode:e.verificationMode||this.config.defaultVerificationMode,hasOverrides:r,timestamp:Date.now()},this.config.environment),l=new URLSearchParams({state:s,sessionId:e.sessionId,mode:this.config.mode});return`${i}/verify?${l.toString()}`}redirect(e){window.location.href=e}openNewTab(e,i){if(this.cleanup(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),this.popupWindow=window.open(e,"safepassage-verify","width=600,height=700"),!this.popupWindow){this.config.onError?.(new Error("Failed to open verification window. Please check popup blocker settings."));return}this.messageListener=n=>{if(!P(n,this.config.environment)){a("POSTMESSAGE_ORIGIN_BLOCKED",{origin:n.origin,environment:this.config.environment,expectedOrigins:`SafePassage trusted origins for ${this.config.environment}`,messageType:n.data?.type});return}let o=T(n,i);if(!o.isValid){a("POSTMESSAGE_VALIDATION_FAILED",{error:o.error,origin:n.origin,sessionId:i.substring(0,8)+"...",messageType:n.data?.type});return}let r={sessionId:n.data.sessionId,status:n.data.status};a("VERIFICATION_COMPLETED",{status:r.status,sessionId:i.substring(0,8)+"...",origin:n.origin}),this.cleanup(),this.unlockVerification(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),r.status==="verified"?this.config.onComplete?.(r):r.status==="cancelled"?this.config.onCancel?.():this.config.onError?.(new Error(`Verification failed: ${r.status}`))},window.addEventListener("message",this.messageListener),this.popupMonitorInterval=setInterval(()=>{this.popupWindow&&this.popupWindow.closed&&(a("POPUP_CLOSED_BY_USER",{sessionId:i.substring(0,8)+"...",environment:this.config.environment}),this.cleanup(),this.unlockVerification(),this.config.onCancel?.())},500)}setupAutoCleanup(){if(this.unloadListener=()=>{a("SDK_AUTO_CLEANUP",{environment:this.config.environment,trigger:"page_unload"}),this.cleanup(),this.unlockVerification()},window.addEventListener("beforeunload",this.unloadListener),window.addEventListener("pagehide",this.unloadListener),window.history&&window.history.pushState){let e=window.history.pushState;window.history.pushState=(...i)=>(this.cleanup(),this.unlockVerification(),e.apply(window.history,i))}}detectEnvironment(){let e=window.location.hostname;return e==="localhost"||e==="127.0.0.1"||e.includes(".local")?"development":e.includes("staging")||e.includes("stage")?"staging":"production"}unlockVerification(){this.isVerificationInProgress=!1,this.currentSessionId=null,a("VERIFICATION_UNLOCKED",{environment:this.config.environment,origin:window.location.origin})}cleanup(){this.popupWindow&&!this.popupWindow.closed&&this.popupWindow.close(),this.popupWindow=null,this.messageListener&&(window.removeEventListener("message",this.messageListener),this.messageListener=null),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null)}removeAutoCleanupListeners(){this.unloadListener&&(window.removeEventListener("beforeunload",this.unloadListener),window.removeEventListener("pagehide",this.unloadListener),this.unloadListener=null)}destroy(){a("SDK_DESTROYED",{environment:this.config.environment,origin:window.location.origin}),this.cleanup(),this.unlockVerification(),this.removeAutoCleanupListeners()}isPublicKey(){return this.config.apiKey.startsWith("pk_")}async createInternalSession(e){let i=crypto.randomUUID();try{let n=this.getPortalApiUrl(),o=await fetch(`${n}/api/v1/sessions/create`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({merchantId:this.config.apiKey,sessionId:i,returnUrl:this.config.returnUrl,cancelUrl:this.config.cancelUrl,challengeAge:e.challengeAge,verificationMode:e.verificationMode,merchantName:document.title||window.location.hostname})});if(!o.ok){let s=await o.json().catch(()=>({}));throw new Error(`Failed to create session: ${o.status} ${o.statusText}. ${s.message||""}`)}let r=await o.json();return a("INTERNAL_SESSION_CREATED",{sessionId:i.substring(0,8)+"...",environment:this.config.environment,apiKeyType:"public"}),i}catch(n){let o=n instanceof Error?n.message:String(n);throw a("INTERNAL_SESSION_FAILED",{error:o,environment:this.config.environment,apiKeyType:"public"}),this.config.onError?.(n),new Error(`Failed to create verification session: ${o}`)}}getPortalApiUrl(){switch(this.config.environment){case"production":return"https://api.safepassageapp.com";case"staging":return"https://api-staging.safepassageapp.com";case"development":default:return"http://localhost:3001"}}},X=c});var ee={};u(ee,{SafePassage:()=>c,VERSION:()=>Q,default:()=>c});S();var Q="3.0.0";typeof window<"u"&&window&&(window.SafePassage=(S(),I(_)).SafePassage);return I(ee);})();
|
|
3
|
-
if(typeof SafePassageSDK !== "undefined" && SafePassageSDK.SafePassage) { window.SafePassage = SafePassageSDK.SafePassage; }
|
|
1
|
+
/* SafePassage SDK v3.0.4 - Redirect Implementation with Enhanced Security */
|
|
2
|
+
"use strict";var SafePassageSDK=(()=>{var u=Object.defineProperty;var G=Object.getOwnPropertyDescriptor;var J=Object.getOwnPropertyNames;var X=Object.prototype.hasOwnProperty;var p=(t,e)=>()=>(t&&(e=t(t=0)),e);var g=(t,e)=>{for(var n in e)u(t,n,{get:e[n],enumerable:!0})},Y=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of J(e))!X.call(t,r)&&r!==n&&u(t,r,{get:()=>e[r],enumerable:!(i=G(e,r))||i.enumerable});return t};var P=t=>Y(u({},"__esModule",{value:!0}),t);function z(t,e){return T[e].includes(t)}function b(t,e,n=[]){let{origin:i}=t;return z(i,e)||n.length>0&&n.some(o=>{if(o.startsWith("*.")){let s=o.slice(2);return i.endsWith(`.${s}`)||i===`https://${s}`||i===`http://${s}`}return i===o})?!0:(console.warn(`SafePassage Security: Blocked PostMessage from untrusted origin: ${i}`,{environment:e,trustedOrigins:T[e],allowedCustomOrigins:n,eventType:t.data?.type}),!1)}function x(t,e){let{data:n}=t;return!n||typeof n!="object"?{isValid:!1,error:"Invalid message format"}:n.type!=="safepassage:verification:complete"?{isValid:!1,error:"Invalid message type"}:!n.sessionId||n.sessionId!==e?{isValid:!1,error:"Session ID mismatch"}:!n.status||!["verified","failed","cancelled"].includes(n.status)?{isValid:!1,error:"Invalid status value"}:{isValid:!0}}function V(t){if(t==="production"&&window.location.protocol!=="https:"){let e=window.location.href.replace("http:","https:");console.error("SafePassage Security: HTTPS required in production. Redirecting...",{current:window.location.href,redirect:e}),window.location.replace(e)}}function h(t,e){try{let n=new URL(t);if(e==="production"&&n.protocol!=="https:")return{isValid:!1,error:"HTTPS required for return URLs in production"};if(e==="development"&&!(n.hostname==="localhost"||n.hostname==="127.0.0.1"||n.hostname.endsWith(".local"))&&n.protocol!=="https:")return{isValid:!1,error:"Non-localhost URLs must use HTTPS"};if(e==="staging"&&n.protocol!=="https:")return{isValid:!1,error:"HTTPS required for return URLs in staging"};let i=[/data:/i,/javascript:/i,/vbscript:/i,/file:/i,/ftp:/i];for(let r of i)if(r.test(t))return{isValid:!1,error:"Blocked suspicious URL scheme"};return{isValid:!0}}catch{return{isValid:!1,error:"Invalid URL format"}}}function a(t,e){console.warn(`SafePassage Security Event: ${t}`,{timestamp:new Date().toISOString(),userAgent:navigator.userAgent,url:window.location.href,...e})}var T,f,L,m=p(()=>{"use strict";T={production:["https://verify.safepassageapp.com","https://portal.safepassageapp.com","https://api.safepassageapp.com"],staging:["https://verify-staging.safepassageapp.com","https://portal-staging.safepassageapp.com","https://api-staging.safepassageapp.com"],development:["http://localhost:5173","http://localhost:3000","http://localhost:3001","http://localhost:3002","http://127.0.0.1:5173","http://127.0.0.1:3000","http://127.0.0.1:3001","http://127.0.0.1:3002"]};f=class{constructor(){this.attempts=new Map;this.maxAttempts=5;this.timeWindow=6e4}isAllowed(e){let n=Date.now(),r=(this.attempts.get(e)||[]).filter(o=>n-o<this.timeWindow);return r.length>=this.maxAttempts?(console.warn(`SafePassage Security: Rate limit exceeded for ${e}`),!1):(r.push(n),this.attempts.set(e,r),!0)}reset(e){this.attempts.delete(e)}},L=new f});var R={};g(R,{createSignedState:()=>Q,generateHMAC:()=>w,generateSecureToken:()=>M,getSigningSecret:()=>v,parseSignedState:()=>ee,verifyHMAC:()=>C});async function w(t,e){let n=new TextEncoder,i=n.encode(e),r=n.encode(t),o=await crypto.subtle.importKey("raw",i,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),s=await crypto.subtle.sign("HMAC",o,r);return Array.from(new Uint8Array(s)).map(l=>l.toString(16).padStart(2,"0")).join("")}async function C(t,e,n){try{let i=await w(t,n);return Z(e,i)}catch{return!1}}function Z(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 M(t=32){let e=new Uint8Array(t);return crypto.getRandomValues(e),Array.from(e,n=>n.toString(16).padStart(2,"0")).join("")}function v(t){return{production:"safepassage-prod-hmac-2025",staging:"safepassage-stage-hmac-2025",development:"safepassage-dev-hmac-2025"}[t]}async function Q(t,e){let n={...t,timestamp:Date.now(),nonce:M(16)},i=JSON.stringify(n),r=v(e),o=await w(i,r);return btoa(JSON.stringify({data:n,signature:o}))}async function ee(t,e,n=D){try{let i=atob(t),r=JSON.parse(i);if(!r.data||!r.signature)return console.warn("SafePassage: Invalid signed state format"),null;let{data:o,signature:s}=r,l=JSON.stringify(o),F=v(e);if(!await C(l,s,F))return console.warn("SafePassage: State signature verification failed"),null;if(o.timestamp){let E=Date.now()-o.timestamp;if(E>n)return console.warn("SafePassage: State parameter expired",{age:E,maxAge:n}),null}let{timestamp:ce,nonce:le,...B}=o;return B}catch(i){return console.warn("SafePassage: Failed to parse signed state",i),null}}var _=p(()=>{"use strict";S()});function k(t){if(!t.apiKey)throw new Error("apiKey is required");if(t.apiKey.length>N)throw new Error(`apiKey exceeds maximum length of ${N} characters`);if(!te.test(t.apiKey))throw new Error("Invalid apiKey format. Expected pk_xxx (public) or sk_xxx (private)");if(typeof window<"u"&&t.apiKey.startsWith("sk_"))throw new Error("Secret keys (sk_) should never be used in browser code for security reasons. Secret keys expose your account to unauthorized access if used client-side. Please use your public key (pk_) instead. If you need to use features that require a secret key (like custom challenge age), create the session server-side and pass the sessionId to startVerificationWithSession(). See: https://docs.safepassageapp.com/server-side-sessions");if(!t.returnUrl)throw new Error("returnUrl is required");if(t.returnUrl.length>d)throw new Error(`returnUrl exceeds maximum length of ${d} characters`);if(!t.cancelUrl)throw new Error("cancelUrl is required");if(t.cancelUrl.length>d)throw new Error(`cancelUrl exceeds maximum length of ${d} characters`);let e=ne(),n=h(t.returnUrl,e);if(!n.isValid)throw new Error(`returnUrl validation failed: ${n.error}`);let i=h(t.cancelUrl,e);if(!i.isValid)throw new Error(`cancelUrl validation failed: ${i.error}`);if(t.defaultChallengeAge!==void 0){if(t.defaultChallengeAge<$)throw new Error(`defaultChallengeAge must be at least ${$}`);if(t.defaultChallengeAge>O)throw new Error(`defaultChallengeAge cannot exceed ${O}`)}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 ne(){let t=window.location.hostname;return t==="localhost"||t==="127.0.0.1"||t.includes(".local")?"development":t.includes("staging")||t.includes("stage")?"staging":"production"}async function K(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(_(),R));return n(t,e)}var $,O,d,N,D,te,S=p(()=>{"use strict";m();$=25,O=150,d=2048,N=128,D=6e5,te=/^(pk_|sk_)[a-zA-Z0-9]+$/});function y(t){let e=ie[t];if((t==="production"||t==="staging")&&!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function re(t){let n={production:"https://api.safepassageapp.com",staging:"https://api-staging.safepassageapp.com",development:"http://localhost:3001"}[t];if((t==="production"||t==="staging")&&!n.startsWith("https://"))throw new Error(`HTTPS required for API URLs in ${t} environment`);return n}function W(t){let e=window.location.protocol==="https:",n=window.location.hostname;switch(t){case"production":if(!e)throw new Error("SafePassage requires HTTPS in production environment");break;case"staging":e||console.warn("SafePassage Warning: HTTPS strongly recommended in staging environment");break;case"development":let i=n==="localhost"||n==="127.0.0.1"||n.includes(".local");!e&&!i&&console.warn("SafePassage Warning: HTTPS recommended for non-localhost development");break}try{y(t),re(t)}catch(i){throw new Error(`Environment configuration validation failed: ${i}`)}}var ie,H=p(()=>{"use strict";ie={production:"https://verify.safepassageapp.com",staging:"https://verify-staging.safepassageapp.com",development:"http://localhost:5173"}});var q={};g(q,{SafePassage:()=>c,default:()=>oe});var c,oe,I=p(()=>{"use strict";S();H();m();c=class{constructor(e){this.popupWindow=null;this.messageListener=null;this.popupMonitorInterval=null;this.unloadListener=null;this.isVerificationInProgress=!1;this.currentSessionId=null;k(e),this.config={...e,environment:e.environment||this.detectEnvironment(),mode:e.mode||"redirect"},W(this.config.environment),V(this.config.environment),a("SDK_INITIALIZED",{environment:this.config.environment,mode:this.config.mode,origin:window.location.origin,protocol:window.location.protocol,hostname:window.location.hostname}),this.setupAutoCleanup()}async verify(e={}){let n=this.isPublicKey(),i=e.sessionId;if(n&&!i&&(i=await this.createInternalSession(e)),!n&&!i)throw new Error("sessionId is required for private API keys - must be a merchant-generated UUID v4");if(!i)throw new Error("Failed to create or obtain sessionId");if(this.isVerificationInProgress){let r=new Error(`Verification already in progress for session ${this.currentSessionId?.substring(0,8)}...`);throw a("RACE_CONDITION_PREVENTED",{currentSession:this.currentSessionId?.substring(0,8)+"...",attemptedSession:e.sessionId?e.sessionId.substring(0,8)+"...":"undefined",origin:window.location.origin}),this.config.onError?.(r),r}this.isVerificationInProgress=!0,this.currentSessionId=i;try{let r=`${this.config.apiKey}:${window.location.origin}`;if(!L.isAllowed(r)){let s=new Error("Too many verification attempts. Please wait before trying again.");throw a("RATE_LIMIT_EXCEEDED",{apiKey:this.config.apiKey.substring(0,8)+"...",origin:window.location.origin,sessionId:i?i.substring(0,8)+"...":"undefined"}),this.config.onError?.(s),s}let o=await this.buildVerificationUrl({...e,sessionId:i});a("VERIFICATION_INITIATED",{environment:this.config.environment,mode:this.config.mode,sessionId:i?i.substring(0,8)+"...":"undefined",origin:window.location.origin}),this.config.mode==="new-tab"?this.openNewTab(o,i):(this.unlockVerification(),this.redirect(o))}catch(r){throw this.unlockVerification(),r}}async buildVerificationUrl(e){let n=y(this.config.environment),i=e.challengeAge!==void 0,r=e.verificationMode!==void 0,o=i||r,s=await K({merchantId:this.config.apiKey,sessionId:e.sessionId,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()},this.config.environment),l=new URLSearchParams({state:s,sessionId:e.sessionId,mode:this.config.mode});return`${n}/verify?${l.toString()}`}redirect(e){window.location.href=e}openNewTab(e,n){if(this.cleanup(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),this.popupWindow=window.open(e,"safepassage-verify","width=600,height=700"),!this.popupWindow){this.config.onError?.(new Error("Failed to open verification window. Please check popup blocker settings."));return}this.messageListener=i=>{if(!b(i,this.config.environment)){a("POSTMESSAGE_ORIGIN_BLOCKED",{origin:i.origin,environment:this.config.environment,expectedOrigins:`SafePassage trusted origins for ${this.config.environment}`,messageType:i.data?.type});return}let r=x(i,n);if(!r.isValid){a("POSTMESSAGE_VALIDATION_FAILED",{error:r.error,origin:i.origin,sessionId:n.substring(0,8)+"...",messageType:i.data?.type});return}let o={sessionId:i.data.sessionId,status:i.data.status};a("VERIFICATION_COMPLETED",{status:o.status,sessionId:n.substring(0,8)+"...",origin:i.origin}),this.cleanup(),this.unlockVerification(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),o.status==="verified"?this.config.onComplete?.(o):o.status==="cancelled"?this.config.onCancel?.():this.config.onError?.(new Error(`Verification failed: ${o.status}`))},window.addEventListener("message",this.messageListener),this.popupMonitorInterval=setInterval(()=>{this.popupWindow&&this.popupWindow.closed&&(a("POPUP_CLOSED_BY_USER",{sessionId:n.substring(0,8)+"...",environment:this.config.environment}),this.cleanup(),this.unlockVerification(),this.config.onCancel?.())},500)}setupAutoCleanup(){if(this.unloadListener=()=>{a("SDK_AUTO_CLEANUP",{environment:this.config.environment,trigger:"page_unload"}),this.cleanup(),this.unlockVerification()},window.addEventListener("beforeunload",this.unloadListener),window.addEventListener("pagehide",this.unloadListener),window.history&&window.history.pushState){let e=window.history.pushState;window.history.pushState=(...n)=>(this.cleanup(),this.unlockVerification(),e.apply(window.history,n))}}detectEnvironment(){let e=window.location.hostname;return e==="localhost"||e==="127.0.0.1"||e.includes(".local")?"development":e.includes("staging")||e.includes("stage")?"staging":"production"}unlockVerification(){this.isVerificationInProgress=!1,this.currentSessionId=null,a("VERIFICATION_UNLOCKED",{environment:this.config.environment,origin:window.location.origin})}cleanup(){this.popupWindow&&!this.popupWindow.closed&&this.popupWindow.close(),this.popupWindow=null,this.messageListener&&(window.removeEventListener("message",this.messageListener),this.messageListener=null),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null)}removeAutoCleanupListeners(){this.unloadListener&&(window.removeEventListener("beforeunload",this.unloadListener),window.removeEventListener("pagehide",this.unloadListener),this.unloadListener=null)}destroy(){a("SDK_DESTROYED",{environment:this.config.environment,origin:window.location.origin}),this.cleanup(),this.unlockVerification(),this.removeAutoCleanupListeners()}isPublicKey(){return this.config.apiKey.startsWith("pk_")}async createInternalSession(e){let n=crypto.randomUUID();try{let i=this.getPortalApiUrl(),r=await fetch(`${i}/api/v1/sessions/create`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({merchantId:this.config.apiKey,sessionId:n,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 s=await r.json().catch(()=>({}));throw new Error(`Failed to create session: ${r.status} ${r.statusText}. ${s.message||""}`)}let o=await r.json();return a("INTERNAL_SESSION_CREATED",{sessionId:n.substring(0,8)+"...",environment:this.config.environment,apiKeyType:"public"}),n}catch(i){let r=i instanceof Error?i.message:String(i);throw a("INTERNAL_SESSION_FAILED",{error:r,environment:this.config.environment,apiKeyType:"public"}),this.config.onError?.(i),new Error(`Failed to create verification session: ${r}`)}}getPortalApiUrl(){switch(this.config.environment){case"production":return"https://api.safepassageapp.com";case"staging":return"https://api-staging.safepassageapp.com";case"development":default:return"http://localhost:3001"}}},oe=c});var se={};g(se,{SafePassage:()=>c,VERSION:()=>j,default:()=>c});function U(){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 A(){let t=[];if(!window.crypto||!window.crypto.getRandomValues)throw new Error("SafePassage SDK requires Web Crypto API support");if(!window.crypto.subtle)throw new Error("SafePassage SDK requires Web Crypto subtle API for HMAC operations");crypto.randomUUID||t.push("crypto.randomUUID not supported, using polyfill"),window.URLSearchParams||t.push("URLSearchParams not supported, consider adding a polyfill for IE 11 support");try{if({a:{b:1}}?.a?.b!==1)throw new Error}catch{t.push("Optional chaining (?.) not supported, ensure transpilation for older browsers")}t.length>0&&console.warn("SafePassage SDK Browser Compatibility:",t.join("; "))}I();typeof window<"u"&&(U(),A());var j="3.0.4";if(typeof window<"u"&&window){let{SafePassage:t}=(I(),P(q));window.SafePassage=t,window.SafePassage.VERSION=j}return P(se);})();
|
|
3
|
+
if(typeof SafePassageSDK !== "undefined" && SafePassageSDK.SafePassage) { window.SafePassage = SafePassageSDK.SafePassage; window.SafePassage.VERSION = SafePassageSDK.VERSION; }
|