@safepassage/sdk 3.4.1 → 3.4.2
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 +232 -198
- package/dist/core/SafePassageSDK.js +3 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/safepassage.min.js +2 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/utils/security.js +1 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
|
-
# SafePassage SDK v3.
|
|
1
|
+
# SafePassage SDK v3.4
|
|
2
2
|
|
|
3
|
-
A lightweight SDK for integrating SafePassage age verification
|
|
4
|
-
|
|
5
|
-
## What's New in v3.0.12
|
|
6
|
-
|
|
7
|
-
- **40% faster launch times**: Removed blocking config fetch, saving 100-300ms
|
|
8
|
-
- **Instant redirects**: SDK now redirects immediately without API calls
|
|
9
|
-
- **Improved reliability**: Eliminates dependency on config endpoint
|
|
3
|
+
A lightweight SDK for integrating SafePassage age verification into your website or application.
|
|
10
4
|
|
|
11
5
|
## Features
|
|
12
6
|
|
|
13
|
-
- **Ultra-lightweight**:
|
|
14
|
-
- **
|
|
15
|
-
- **Simple integration**: Just 10 lines of code
|
|
7
|
+
- **Ultra-lightweight**: ~18KB minified
|
|
8
|
+
- **Simple integration**: 5 lines of code to get started
|
|
16
9
|
- **Two modes**: Same-tab redirect or new-tab popup
|
|
17
10
|
- **TypeScript support**: Full type definitions included
|
|
18
|
-
- **Auto-environment detection**: Works seamlessly
|
|
19
|
-
- **Secure**:
|
|
11
|
+
- **Auto-environment detection**: Works seamlessly across environments
|
|
12
|
+
- **Secure**: HMAC-signed state parameters, automatic session management
|
|
20
13
|
- **Compliant**: Enforces minimum age of 25
|
|
21
14
|
|
|
22
15
|
## Installation
|
|
@@ -25,286 +18,327 @@ A lightweight SDK for integrating SafePassage age verification using a simple re
|
|
|
25
18
|
npm install @safepassage/sdk
|
|
26
19
|
```
|
|
27
20
|
|
|
21
|
+
Or load directly from jsDelivr CDN (no bundler required):
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<script src="https://cdn.jsdelivr.net/npm/@safepassage/sdk@latest/dist/safepassage.min.js"></script>
|
|
25
|
+
```
|
|
26
|
+
|
|
28
27
|
## Quick Start
|
|
29
28
|
|
|
29
|
+
### With npm/bundler
|
|
30
|
+
|
|
30
31
|
```javascript
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
import { SafePassage } from '@safepassage/sdk';
|
|
33
|
+
|
|
34
|
+
const sp = new SafePassage({
|
|
35
|
+
apiKey: 'pk_...', // Your public key from the dashboard
|
|
36
|
+
returnUrl: window.location.origin + '/verified'
|
|
35
37
|
});
|
|
36
38
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
+
// Start verification - redirects user to SafePassage
|
|
40
|
+
await sp.verify();
|
|
41
|
+
```
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
### With CDN (no bundler)
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<script src="https://cdn.jsdelivr.net/npm/@safepassage/sdk@latest/dist/safepassage.min.js"></script>
|
|
47
|
+
<script>
|
|
48
|
+
const sp = new SafePassage({
|
|
49
|
+
apiKey: 'pk_...',
|
|
50
|
+
returnUrl: window.location.origin + '/verified'
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
document.getElementById('verify-btn').onclick = () => sp.verify();
|
|
54
|
+
</script>
|
|
46
55
|
```
|
|
47
56
|
|
|
57
|
+
That's it! The SDK handles session creation automatically.
|
|
58
|
+
|
|
48
59
|
## Configuration
|
|
49
60
|
|
|
50
61
|
| Option | Type | Required | Description |
|
|
51
62
|
|--------|------|----------|-------------|
|
|
52
|
-
| apiKey | string | Yes | Your API key (
|
|
53
|
-
| returnUrl | string | Yes | URL to redirect after
|
|
54
|
-
| environment | string | No | 'production'
|
|
55
|
-
| mode | string | No | 'redirect' (default) or 'new-tab' |
|
|
56
|
-
|
|
|
63
|
+
| apiKey | string | Yes | Your public API key (`pk_...`) |
|
|
64
|
+
| returnUrl | string | Yes | URL to redirect after verification |
|
|
65
|
+
| environment | string | No | `'production'` or `'staging'` (auto-detected) |
|
|
66
|
+
| mode | string | No | `'redirect'` (default) or `'new-tab'` |
|
|
67
|
+
| defaultChallengeAge | number | No | Default minimum age (25 or higher) |
|
|
68
|
+
| defaultVerificationMode | string | No | `'L1'` or `'L2'` |
|
|
69
|
+
| onComplete | function | No | Callback for new-tab mode |
|
|
70
|
+
| onCancel | function | No | Called when user closes popup (new-tab mode) |
|
|
57
71
|
| onError | function | No | Error handler |
|
|
58
72
|
|
|
59
|
-
### API Key Types
|
|
60
|
-
|
|
61
|
-
SafePassage provides two types of API keys:
|
|
62
|
-
|
|
63
|
-
- **Public Keys (`pk_`)**: Safe for client-side use (websites, mobile apps)
|
|
64
|
-
- Limited to creating and initiating verifications
|
|
65
|
-
- Cannot read verification results or override settings
|
|
66
|
-
- SDK auto-generates sessionId if not provided
|
|
67
|
-
|
|
68
|
-
- **Secret Keys (`sk_`)**: Server-side only - keep these private!
|
|
69
|
-
- Full API access including reading verification results
|
|
70
|
-
- Can override challenge age and verification mode
|
|
71
|
-
- Requires merchant-generated sessionId
|
|
72
|
-
|
|
73
73
|
## Verification Options
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
safePassage.verify({
|
|
77
|
-
sessionId: 'uuid-v4', // Required for sk_ keys, optional for pk_ keys
|
|
78
|
-
challengeAge: 30, // Optional: min 25 (sk_ keys only)
|
|
79
|
-
verificationMode: 'L2' // Optional: 'L1' or 'L2' (sk_ keys only)
|
|
80
|
-
});
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Configuration Override Behavior
|
|
84
|
-
|
|
85
|
-
When you pass `challengeAge` or `verificationMode` to the `verify()` method, these values take precedence over your dashboard configuration for that specific verification session:
|
|
75
|
+
Override settings per-verification:
|
|
86
76
|
|
|
87
|
-
- **No overrides**: Uses your current dashboard settings
|
|
88
|
-
- **With overrides**: SDK values are used instead of dashboard settings
|
|
89
|
-
- **Challenge age**: Must be 25 or higher (lower values will be rejected)
|
|
90
|
-
- **Verification mode**:
|
|
91
|
-
- `'L1'`: Age estimation with computer vision
|
|
92
|
-
- `'L2'`: Always requires ID verification
|
|
93
|
-
|
|
94
|
-
Example use cases:
|
|
95
77
|
```javascript
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
challengeAge: 30 // Require age 30+ for this session
|
|
78
|
+
await sp.verify({
|
|
79
|
+
challengeAge: 30, // Override minimum age for this session
|
|
80
|
+
verificationMode: 'L2', // Force ID verification for this session
|
|
81
|
+
externalUserId: 'user-123', // Your user ID (returned in webhooks)
|
|
82
|
+
skipIntro: true, // Skip intro screen
|
|
83
|
+
autoReturn: true // Auto-redirect after success
|
|
103
84
|
});
|
|
85
|
+
```
|
|
104
86
|
|
|
105
|
-
|
|
106
|
-
safePassage.verify({
|
|
107
|
-
sessionId: crypto.randomUUID(),
|
|
108
|
-
verificationMode: 'L2' // Force ID check for this session
|
|
109
|
-
});
|
|
87
|
+
### Verification Modes
|
|
110
88
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
sessionId: crypto.randomUUID(),
|
|
114
|
-
challengeAge: 30,
|
|
115
|
-
verificationMode: 'L2' // ID required for 30+ verification
|
|
116
|
-
});
|
|
117
|
-
```
|
|
89
|
+
- **L1**: Age estimation using computer vision (faster, less friction)
|
|
90
|
+
- **L2**: Full ID document verification (more thorough)
|
|
118
91
|
|
|
119
|
-
## Modes
|
|
92
|
+
## Integration Modes
|
|
120
93
|
|
|
121
94
|
### Same-Tab Redirect (Default)
|
|
122
|
-
|
|
95
|
+
|
|
96
|
+
User is redirected to SafePassage, then back to your `returnUrl`:
|
|
123
97
|
|
|
124
98
|
```javascript
|
|
125
|
-
const
|
|
126
|
-
apiKey: '
|
|
99
|
+
const sp = new SafePassage({
|
|
100
|
+
apiKey: 'pk_...',
|
|
127
101
|
returnUrl: '/age-verified'
|
|
128
102
|
});
|
|
129
103
|
|
|
130
|
-
|
|
104
|
+
await sp.verify();
|
|
105
|
+
// User is redirected to SafePassage...
|
|
106
|
+
// After verification, user returns to /age-verified?sessionId=xxx&status=verified
|
|
131
107
|
```
|
|
132
108
|
|
|
133
109
|
### New-Tab Mode
|
|
110
|
+
|
|
134
111
|
Verification opens in a popup window:
|
|
135
112
|
|
|
136
113
|
```javascript
|
|
137
|
-
const
|
|
138
|
-
apiKey: '
|
|
114
|
+
const sp = new SafePassage({
|
|
115
|
+
apiKey: 'pk_...',
|
|
139
116
|
returnUrl: '/age-verified',
|
|
140
117
|
mode: 'new-tab',
|
|
141
118
|
onComplete: (result) => {
|
|
142
|
-
console.log('
|
|
143
|
-
// Validate on your server
|
|
119
|
+
console.log('Verification complete:', result.sessionId, result.status);
|
|
120
|
+
// Validate on your server, then update UI
|
|
121
|
+
},
|
|
122
|
+
onCancel: () => {
|
|
123
|
+
console.log('User closed the verification window');
|
|
124
|
+
},
|
|
125
|
+
onError: (error) => {
|
|
126
|
+
console.error('Verification error:', error.message);
|
|
144
127
|
}
|
|
145
128
|
});
|
|
146
129
|
|
|
147
|
-
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
## URL Query Parameters
|
|
151
|
-
|
|
152
|
-
SafePassage supports optional query parameters for streamlined verification flows:
|
|
153
|
-
|
|
154
|
-
### skip_intro
|
|
155
|
-
Skip the introductory screen and navigate directly to camera access:
|
|
156
|
-
|
|
157
|
-
```javascript
|
|
158
|
-
// Via SDK (automatic)
|
|
159
|
-
safePassage.verify({ sessionId: crypto.randomUUID() });
|
|
160
|
-
|
|
161
|
-
// Via server-side session creation
|
|
162
|
-
const url = new URL(verifyUrl);
|
|
163
|
-
url.searchParams.set('skip_intro', 'true');
|
|
164
|
-
window.location.href = url.toString();
|
|
130
|
+
await sp.verify();
|
|
165
131
|
```
|
|
166
132
|
|
|
167
|
-
|
|
168
|
-
Automatically redirect to `returnUrl` after successful verification (redirect mode only):
|
|
133
|
+
## Server-Side Validation (Required!)
|
|
169
134
|
|
|
170
|
-
|
|
171
|
-
// Via server-side session creation
|
|
172
|
-
const url = new URL(verifyUrl);
|
|
173
|
-
url.searchParams.set('auto_return', 'true');
|
|
174
|
-
window.location.href = url.toString();
|
|
175
|
-
```
|
|
135
|
+
After verification completes, **always validate the result on your server** before granting access:
|
|
176
136
|
|
|
177
|
-
### Combined Example
|
|
178
137
|
```javascript
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
138
|
+
// Node.js / Express example
|
|
139
|
+
app.get('/age-verified', async (req, res) => {
|
|
140
|
+
const { sessionId } = req.query;
|
|
141
|
+
|
|
142
|
+
// Validate with your SECRET key (sk_...)
|
|
143
|
+
const response = await fetch(
|
|
144
|
+
`https://api.safepassageapp.com/api/v1/sessions/${sessionId}`,
|
|
145
|
+
{
|
|
146
|
+
headers: {
|
|
147
|
+
'Authorization': `Bearer ${process.env.SAFEPASSAGE_SECRET_KEY}`
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
);
|
|
189
151
|
|
|
190
|
-
|
|
152
|
+
const session = await response.json();
|
|
191
153
|
|
|
192
|
-
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
'
|
|
198
|
-
|
|
199
|
-
},
|
|
200
|
-
body: JSON.stringify({ sessionId })
|
|
154
|
+
if (session.status === 'VERIFIED') {
|
|
155
|
+
// Grant access
|
|
156
|
+
req.session.ageVerified = true;
|
|
157
|
+
res.redirect('/content');
|
|
158
|
+
} else {
|
|
159
|
+
res.redirect('/age-verification-failed');
|
|
160
|
+
}
|
|
201
161
|
});
|
|
202
|
-
|
|
203
|
-
const result = await response.json();
|
|
204
|
-
if (result.verified && result.estimatedAge >= result.challengeAge) {
|
|
205
|
-
// Grant access
|
|
206
|
-
}
|
|
207
162
|
```
|
|
208
163
|
|
|
209
|
-
|
|
164
|
+
> **Security Note**: Never trust client-side verification status alone. Always validate server-side using your secret key.
|
|
165
|
+
|
|
166
|
+
## Webhooks (Recommended)
|
|
210
167
|
|
|
211
|
-
|
|
168
|
+
For reliable verification tracking, configure webhooks in your dashboard:
|
|
212
169
|
|
|
213
170
|
```javascript
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
222
|
-
return v.toString(16);
|
|
223
|
-
});
|
|
171
|
+
// Webhook payload example
|
|
172
|
+
{
|
|
173
|
+
"event": "verification.completed",
|
|
174
|
+
"sessionId": "abc-123",
|
|
175
|
+
"verified": true,
|
|
176
|
+
"externalUserId": "your-user-id", // If provided during verify()
|
|
177
|
+
"timestamp": "2025-01-15T10:30:00Z"
|
|
224
178
|
}
|
|
225
179
|
```
|
|
226
180
|
|
|
227
181
|
## Complete Example
|
|
228
182
|
|
|
183
|
+
### HTML + CDN
|
|
184
|
+
|
|
229
185
|
```html
|
|
230
186
|
<!DOCTYPE html>
|
|
231
187
|
<html>
|
|
232
188
|
<head>
|
|
233
|
-
|
|
234
|
-
<script src="
|
|
189
|
+
<title>Age Verification</title>
|
|
190
|
+
<script src="https://cdn.jsdelivr.net/npm/@safepassage/sdk@latest/dist/safepassage.min.js"></script>
|
|
235
191
|
</head>
|
|
236
192
|
<body>
|
|
237
|
-
<button
|
|
193
|
+
<button id="verify-btn">Verify Your Age</button>
|
|
238
194
|
|
|
239
195
|
<script>
|
|
240
|
-
const
|
|
241
|
-
apiKey: '
|
|
242
|
-
returnUrl: window.location.
|
|
196
|
+
const sp = new SafePassage({
|
|
197
|
+
apiKey: 'pk_...',
|
|
198
|
+
returnUrl: window.location.origin + '/verified'
|
|
243
199
|
});
|
|
244
200
|
|
|
245
|
-
|
|
246
|
-
// Use crypto.randomUUID() if available, otherwise fallback
|
|
247
|
-
const sessionId = typeof crypto.randomUUID === 'function'
|
|
248
|
-
? crypto.randomUUID()
|
|
249
|
-
: 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
250
|
-
const r = Math.random() * 16 | 0;
|
|
251
|
-
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
252
|
-
return v.toString(16);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
sessionStorage.setItem('pendingVerification', sessionId);
|
|
256
|
-
safePassage.verify({ sessionId });
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Check if returning from verification
|
|
260
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
261
|
-
if (urlParams.get('verified') === 'true') {
|
|
262
|
-
const sessionId = sessionStorage.getItem('pendingVerification');
|
|
263
|
-
// Validate session server-side here
|
|
264
|
-
console.log('Validate session:', sessionId);
|
|
265
|
-
}
|
|
201
|
+
document.getElementById('verify-btn').onclick = () => sp.verify();
|
|
266
202
|
</script>
|
|
267
203
|
</body>
|
|
268
204
|
</html>
|
|
269
205
|
```
|
|
270
206
|
|
|
207
|
+
### React Component
|
|
208
|
+
|
|
209
|
+
```jsx
|
|
210
|
+
import { useState } from 'react';
|
|
211
|
+
import { SafePassage } from '@safepassage/sdk';
|
|
212
|
+
|
|
213
|
+
function AgeGate() {
|
|
214
|
+
const [verifying, setVerifying] = useState(false);
|
|
215
|
+
|
|
216
|
+
const sp = new SafePassage({
|
|
217
|
+
apiKey: process.env.NEXT_PUBLIC_SAFEPASSAGE_KEY,
|
|
218
|
+
returnUrl: window.location.origin + '/verified'
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const handleVerify = async () => {
|
|
222
|
+
setVerifying(true);
|
|
223
|
+
try {
|
|
224
|
+
await sp.verify();
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error('Failed to start verification:', error);
|
|
227
|
+
setVerifying(false);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<button onClick={handleVerify} disabled={verifying}>
|
|
233
|
+
{verifying ? 'Redirecting...' : 'Verify Your Age'}
|
|
234
|
+
</button>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
271
239
|
## TypeScript
|
|
272
240
|
|
|
273
241
|
Full TypeScript support included:
|
|
274
242
|
|
|
275
243
|
```typescript
|
|
276
|
-
import { SafePassage, SafePassageConfig } from '@safepassage/sdk';
|
|
244
|
+
import { SafePassage, SafePassageConfig, VerificationResult } from '@safepassage/sdk';
|
|
277
245
|
|
|
278
246
|
const config: SafePassageConfig = {
|
|
279
|
-
apiKey: process.env.
|
|
280
|
-
returnUrl: '/verified'
|
|
247
|
+
apiKey: process.env.NEXT_PUBLIC_SAFEPASSAGE_KEY!,
|
|
248
|
+
returnUrl: '/verified',
|
|
249
|
+
mode: 'new-tab',
|
|
250
|
+
onComplete: (result: VerificationResult) => {
|
|
251
|
+
console.log(`Session ${result.sessionId}: ${result.status}`);
|
|
252
|
+
}
|
|
281
253
|
};
|
|
282
254
|
|
|
283
|
-
const
|
|
255
|
+
const sp = new SafePassage(config);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Skip Parameters
|
|
259
|
+
|
|
260
|
+
For streamlined embedded flows:
|
|
261
|
+
|
|
262
|
+
```javascript
|
|
263
|
+
// Skip intro screen (go directly to camera)
|
|
264
|
+
await sp.verify({ skipIntro: true });
|
|
265
|
+
|
|
266
|
+
// Auto-redirect after success (no success screen)
|
|
267
|
+
await sp.verify({ autoReturn: true });
|
|
268
|
+
|
|
269
|
+
// Both - minimal user interaction
|
|
270
|
+
await sp.verify({ skipIntro: true, autoReturn: true });
|
|
284
271
|
```
|
|
285
272
|
|
|
286
|
-
##
|
|
273
|
+
## Error Handling
|
|
287
274
|
|
|
288
|
-
New streamlined approach (10 lines):
|
|
289
275
|
```javascript
|
|
290
|
-
const
|
|
291
|
-
apiKey: '
|
|
292
|
-
returnUrl: '/verified'
|
|
276
|
+
const sp = new SafePassage({
|
|
277
|
+
apiKey: 'pk_...',
|
|
278
|
+
returnUrl: '/verified',
|
|
279
|
+
onError: (error) => {
|
|
280
|
+
if (error.message.includes('popup')) {
|
|
281
|
+
alert('Please allow popups for age verification');
|
|
282
|
+
} else if (error.message.includes('rate limit')) {
|
|
283
|
+
alert('Too many attempts. Please wait a moment.');
|
|
284
|
+
} else {
|
|
285
|
+
console.error('Verification error:', error);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
293
288
|
});
|
|
294
|
-
safePassage.verify({ sessionId: crypto.randomUUID() });
|
|
295
289
|
```
|
|
296
290
|
|
|
291
|
+
## API Keys
|
|
292
|
+
|
|
293
|
+
SafePassage uses two types of API keys:
|
|
294
|
+
|
|
295
|
+
| Key Type | Prefix | Use Case |
|
|
296
|
+
|----------|--------|----------|
|
|
297
|
+
| Public Key | `pk_` | Client-side SDK (this package) |
|
|
298
|
+
| Secret Key | `sk_` | Server-side validation only |
|
|
299
|
+
|
|
300
|
+
> **Important**: This SDK only works with public keys (`pk_`). For server-side integrations using secret keys, use the [Direct API](https://docs.safepassageapp.com/api) instead.
|
|
301
|
+
|
|
297
302
|
## Browser Support
|
|
298
303
|
|
|
299
304
|
- Chrome 60+
|
|
300
305
|
- Firefox 60+
|
|
301
306
|
- Safari 12+
|
|
302
307
|
- Edge 79+
|
|
303
|
-
- Mobile browsers
|
|
308
|
+
- Mobile browsers (iOS Safari, Chrome for Android)
|
|
309
|
+
|
|
310
|
+
## Security Best Practices
|
|
311
|
+
|
|
312
|
+
1. **Use public keys client-side** - Never expose secret keys in browser code
|
|
313
|
+
2. **Validate server-side** - Always verify results using your secret key
|
|
314
|
+
3. **Configure webhooks** - For reliable, tamper-proof verification notifications
|
|
315
|
+
4. **Register callback URLs** - Pre-register your `returnUrl` in the dashboard
|
|
316
|
+
5. **Use HTTPS** - SDK enforces HTTPS in production
|
|
317
|
+
|
|
318
|
+
## Cleanup
|
|
319
|
+
|
|
320
|
+
When done with the SDK (e.g., in SPA route changes):
|
|
321
|
+
|
|
322
|
+
```javascript
|
|
323
|
+
sp.destroy();
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Migration from v3.0.x
|
|
327
|
+
|
|
328
|
+
If you were using merchant-generated session IDs:
|
|
329
|
+
|
|
330
|
+
```javascript
|
|
331
|
+
// Old (v3.0.x)
|
|
332
|
+
safePassage.verify({ sessionId: crypto.randomUUID() });
|
|
333
|
+
|
|
334
|
+
// New (v3.4+) - sessionId is auto-generated
|
|
335
|
+
await sp.verify();
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
The SDK now creates sessions automatically via the API when using public keys.
|
|
304
339
|
|
|
305
|
-
##
|
|
340
|
+
## Support
|
|
306
341
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
4. Never expose your secret key (sk_xxx)
|
|
342
|
+
- [Documentation](https://docs.safepassageapp.com)
|
|
343
|
+
- [API Reference](https://docs.safepassageapp.com/api)
|
|
344
|
+
- [Dashboard](https://portal.safepassageapp.com)
|
|
@@ -285,7 +285,7 @@ export class SafePassage {
|
|
|
285
285
|
}
|
|
286
286
|
// Set up PostMessage listener with enhanced security
|
|
287
287
|
this.messageListener = (event) => {
|
|
288
|
-
var _a, _b, _c, _d, _e, _f
|
|
288
|
+
var _a, _b, _c, _d, _e, _f;
|
|
289
289
|
// Enhanced origin validation with strict allowlist
|
|
290
290
|
if (!validatePostMessageOrigin(event, this.config.environment)) {
|
|
291
291
|
logSecurityEvent('POSTMESSAGE_ORIGIN_BLOCKED', {
|
|
@@ -330,11 +330,9 @@ export class SafePassage {
|
|
|
330
330
|
if (result.status === 'verified') {
|
|
331
331
|
(_d = (_c = this.config).onComplete) === null || _d === void 0 ? void 0 : _d.call(_c, result);
|
|
332
332
|
}
|
|
333
|
-
else if (result.status === 'cancelled') {
|
|
334
|
-
(_f = (_e = this.config).onCancel) === null || _f === void 0 ? void 0 : _f.call(_e);
|
|
335
|
-
}
|
|
336
333
|
else {
|
|
337
|
-
|
|
334
|
+
// Status is 'failed' - trigger error callback
|
|
335
|
+
(_f = (_e = this.config).onError) === null || _f === void 0 ? void 0 : _f.call(_e, new Error(`Verification failed: ${result.status}`));
|
|
338
336
|
}
|
|
339
337
|
};
|
|
340
338
|
window.addEventListener('message', this.messageListener);
|
package/dist/index.d.ts
CHANGED
|
@@ -18,4 +18,4 @@
|
|
|
18
18
|
*/
|
|
19
19
|
export { SafePassage, SafePassage as default } from './core/SafePassageSDK';
|
|
20
20
|
export type { SafePassageConfig, VerificationOptions, VerificationResult, SessionValidationResponse, } from './types';
|
|
21
|
-
export declare const VERSION = "3.4.
|
|
21
|
+
export declare const VERSION = "3.4.2";
|
package/dist/index.js
CHANGED
|
@@ -24,8 +24,8 @@ if (typeof window !== 'undefined') {
|
|
|
24
24
|
checkBrowserCompatibility();
|
|
25
25
|
}
|
|
26
26
|
export { SafePassage, SafePassage as default } from './core/SafePassageSDK';
|
|
27
|
-
// Version - 3.4.
|
|
28
|
-
export const VERSION = '3.4.
|
|
27
|
+
// Version - 3.4.2: Fixed CDN references, aligned documentation
|
|
28
|
+
export const VERSION = '3.4.2';
|
|
29
29
|
// For UMD builds
|
|
30
30
|
if (typeof window !== 'undefined' && window) {
|
|
31
31
|
// Dynamic import for UMD builds
|
package/dist/safepassage.min.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/* SafePassage SDK v3.4.
|
|
2
|
-
"use strict";var SafePassageSDK=(()=>{var S=Object.defineProperty,oe=Object.defineProperties,ae=Object.getOwnPropertyDescriptor,ce=Object.getOwnPropertyDescriptors,le=Object.getOwnPropertyNames,v=Object.getOwnPropertySymbols;var P=Object.prototype.hasOwnProperty,C=Object.prototype.propertyIsEnumerable;var R=(t,e,n)=>e in t?S(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,h=(t,e)=>{for(var n in e||(e={}))P.call(e,n)&&R(t,n,e[n]);if(v)for(var n of v(e))C.call(e,n)&&R(t,n,e[n]);return t},y=(t,e)=>oe(t,ce(e));var D=(t,e)=>{var n={};for(var i in t)P.call(t,i)&&e.indexOf(i)<0&&(n[i]=t[i]);if(t!=null&&v)for(var i of v(t))e.indexOf(i)<0&&C.call(t,i)&&(n[i]=t[i]);return n};var w=(t,e)=>()=>(t&&(e=t(t=0)),e);var U=(t,e)=>{for(var n in e)S(t,n,{get:e[n],enumerable:!0})},pe=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of le(e))!P.call(t,r)&&r!==n&&S(t,r,{get:()=>e[r],enumerable:!(i=ae(e,r))||i.enumerable});return t};var $=t=>pe(S({},"__esModule",{value:!0}),t);function de(t,e){return K[e].includes(t)}function W(t,e,n=[]){var r;let{origin:i}=t;return de(i,e)||n.length>0&&n.some(o=>{if(o.startsWith("*.")){let a=o.slice(2);return i.endsWith(`.${a}`)||i===`https://${a}`||i===`http://${a}`}return i===o})?!0:(console.warn(`SafePassage Security: Blocked PostMessage from untrusted origin: ${i}`,{environment:e,trustedOrigins:K[e],allowedCustomOrigins:n,eventType:(r=t.data)==null?void 0:r.type}),!1)}function H(t,e){let{data:n}=t;return!n||typeof n!="object"?{isValid:!1,error:"Invalid message format"}:n.type!=="safepassage:verification:complete"?{isValid:!1,error:"Invalid message type"}:!n.sessionId||n.sessionId!==e?{isValid:!1,error:"Session ID mismatch"}:!n.status||!["verified","failed","cancelled"].includes(n.status)?{isValid:!1,error:"Invalid status value"}:{isValid:!0}}function j(t){t==="production"&&window.location.protocol!=="https:"&&console.warn("SafePassage Warning: HTTPS recommended for production environment",{current:window.location.href})}function T(t,e){try{let n=new URL(t);if(n.protocol!=="https:"&&!(n.hostname==="localhost"||n.hostname==="127.0.0.1"))return{isValid:!1,error:`HTTPS required for return URLs in ${e}`};let i=[/data:/i,/javascript:/i,/vbscript:/i,/file:/i,/ftp:/i];for(let r of i)if(r.test(t))return{isValid:!1,error:"Blocked suspicious URL scheme"};return{isValid:!0}}catch(n){return{isValid:!1,error:"Invalid URL format"}}}function l(t,e){console.warn(`SafePassage Security Event: ${t}`,h({timestamp:new Date().toISOString(),userAgent:navigator.userAgent,url:window.location.href},e))}var K,A,q,b=w(()=>{"use strict";K={production:["https://av.safepassageapp.com","https://portal.safepassageapp.com","https://api.safepassageapp.com"],staging:["https://av.staging.safepassageapp.com","https://portal.staging.safepassageapp.com","https://api.staging.safepassageapp.com"]};A=class{constructor(){this.attempts=new Map;this.maxAttempts=5;this.timeWindow=6e4}isAllowed(e){let n=Date.now(),r=(this.attempts.get(e)||[]).filter(s=>n-s<this.timeWindow);return r.length>=this.maxAttempts?(console.warn(`SafePassage Security: Rate limit exceeded for ${e}`),!1):(r.push(n),this.attempts.set(e,r),!0)}reset(e){this.attempts.delete(e)}},q=new A});var G={};U(G,{createSignedState:()=>ge,generateHMAC:()=>x,generateSecureToken:()=>B,getSigningSecret:()=>V,parseSignedState:()=>fe,verifyHMAC:()=>F});async function x(t,e){let n=new TextEncoder,i=n.encode(e),r=n.encode(t),s=await crypto.subtle.importKey("raw",i,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),o=await crypto.subtle.sign("HMAC",s,r);return Array.from(new Uint8Array(o)).map(a=>a.toString(16).padStart(2,"0")).join("")}async function F(t,e,n){try{let i=await x(t,n);return ue(e,i)}catch(i){return!1}}function ue(t,e){if(t.length!==e.length)return!1;let n=0;for(let i=0;i<t.length;i++)n|=t.charCodeAt(i)^e.charCodeAt(i);return n===0}function B(t=32){let e=new Uint8Array(t);return crypto.getRandomValues(e),Array.from(e,n=>n.toString(16).padStart(2,"0")).join("")}function V(t){return{production:"safepassage-prod-hmac-2025",staging:"safepassage-stage-hmac-2025"}[t]}async function ge(t,e){let n=y(h({},t),{timestamp:Date.now(),nonce:B(16)}),i=JSON.stringify(n),r=V(e),s=await x(i,r);return btoa(JSON.stringify({data:n,signature:s}))}async function fe(t,e,n=X){try{let r=atob(t),s=JSON.parse(r);if(!s.data||!s.signature)return console.warn("SafePassage: Invalid signed state format"),null;let{data:o,signature:a}=s,p=JSON.stringify(o),c=V(e);if(!await F(p,a,c))return console.warn("SafePassage: State signature verification failed"),null;if(o.timestamp){let m=Date.now()-o.timestamp;if(m>n)return console.warn("SafePassage: State parameter expired",{age:m,maxAge:n}),null}let i=o,{timestamp:g,nonce:f}=i;return D(i,["timestamp","nonce"])}catch(r){return console.warn("SafePassage: Failed to parse signed state",r),null}}var J=w(()=>{"use strict";k()});function Q(t){if(!t.apiKey)throw new Error("apiKey is required");if(t.apiKey.length>Z)throw new Error(`apiKey exceeds maximum length of ${Z} characters`);if(!he.test(t.apiKey))throw new Error("Invalid apiKey format. Expected pk_xxx (public) or sk_xxx (private)");if(typeof window!="undefined"&&t.apiKey.startsWith("sk_"))throw new Error("Secret keys (sk_) should never be used in browser code for security reasons. Secret keys expose your account to unauthorized access if used client-side. Please use your public key (pk_) instead. If you need to use features that require a secret key (like custom challenge age), create the session server-side and pass the sessionId to startVerificationWithSession(). See: https://docs.safepassageapp.com/server-side-sessions");if(!t.returnUrl)throw new Error("returnUrl is required");if(t.returnUrl.length>E)throw new Error(`returnUrl exceeds maximum length of ${E} characters`);let e=me(),n=T(t.returnUrl,e);if(!n.isValid)throw new Error(`returnUrl validation failed: ${n.error}`);if(t.cancelUrl){if(t.cancelUrl.length>E)throw new Error(`cancelUrl exceeds maximum length of ${E} characters`);let i=T(t.cancelUrl,e);if(!i.isValid)throw new Error(`cancelUrl validation failed: ${i.error}`)}if(t.defaultChallengeAge!==void 0){if(t.defaultChallengeAge<Y)throw new Error(`defaultChallengeAge must be at least ${Y}`);if(t.defaultChallengeAge>z)throw new Error(`defaultChallengeAge cannot exceed ${z}`)}if(t.defaultVerificationMode&&!["L1","L2"].includes(t.defaultVerificationMode))throw new Error("defaultVerificationMode must be L1 or L2");if(t.mode&&!["redirect","new-tab"].includes(t.mode))throw new Error("mode must be redirect or new-tab")}function me(){if(typeof window=="undefined")return"production";let t=window.location.hostname;return t.includes("staging")||t.includes("stage")?"staging":"production"}async function ee(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(J(),G));return n(t,e)}var Y,z,E,Z,X,he,k=w(()=>{"use strict";b();Y=25,z=150,E=2048,Z=128,X=6e5,he=/^(pk_|sk_)[a-zA-Z0-9_]+$/});function M(t){let e=te[t]||te.production;if(!e||!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function we(t){let e={production:"https://api.safepassageapp.com",staging:"https://api.staging.safepassageapp.com"},n=e[t]||e.production;if(!n||!n.startsWith("https://"))throw new Error(`HTTPS required for API URLs in ${t} environment`);return n}function ne(t){let e=window.location.protocol==="https:";switch(t){case"production":e||console.warn("SafePassage Warning: HTTPS recommended for production environment");break;case"staging":e||console.warn("SafePassage Warning: HTTPS strongly recommended in staging environment");break}try{M(t),we(t)}catch(n){let i=n instanceof Error?n.message:String(n);throw new Error(`Environment configuration validation failed: ${i}`)}}var te,ie=w(()=>{"use strict";te={production:"https://av.safepassageapp.com",staging:"https://av.staging.safepassageapp.com"}});var re={};U(re,{SafePassage:()=>u,default:()=>ve});var u,ve,_=w(()=>{"use strict";k();ie();b();u=class{constructor(e){this.popupWindow=null;this.messageListener=null;this.popupMonitorInterval=null;this.unloadListener=null;this.isVerificationInProgress=!1;this.currentSessionId=null;this.lastVerifyUrl=null;this.lastSessionToken=null;Q(e);let n=e.environment||this.detectEnvironment();n!=="staging"&&n!=="production"&&(console.warn(`SafePassage SDK: Unknown environment '${n}', defaulting to 'production'`),n="production"),this.config=y(h({},e),{environment:n,mode:e.mode||"redirect"}),ne(this.config.environment),j(this.config.environment),l("SDK_INITIALIZED",{environment:this.config.environment,mode:this.config.mode,origin:window.location.origin,protocol:window.location.protocol,hostname:window.location.hostname}),this.setupAutoCleanup()}async verify(e={}){var r,s,o,a,p,c;let n=this.isPublicKey(),i;if(n)i=await this.createInternalSession(e);else throw new Error("Private API keys (sk_) should use the direct API, not the SDK. The SDK is designed for browser-based public key usage only.");if(!i)throw new Error("Failed to obtain sessionId from server");if(this.isVerificationInProgress){let d=new Error(`Verification already in progress for session ${(r=this.currentSessionId)==null?void 0:r.substring(0,8)}...`);throw l("RACE_CONDITION_PREVENTED",{currentSession:((s=this.currentSessionId)==null?void 0:s.substring(0,8))+"...",attemptedSession:"new-session-attempt",origin:window.location.origin}),(a=(o=this.config).onError)==null||a.call(o,d),d}this.isVerificationInProgress=!0,this.currentSessionId=i;try{let d=`${this.config.apiKey}:${window.location.origin}`;if(!q.isAllowed(d)){let f=new Error("Too many verification attempts. Please wait before trying again.");throw l("RATE_LIMIT_EXCEEDED",{apiKey:this.config.apiKey.substring(0,8)+"...",origin:window.location.origin,sessionId:i?i.substring(0,8)+"...":"undefined"}),(c=(p=this.config).onError)==null||c.call(p,f),f}let g=await this.buildVerificationUrl(e,i);l("VERIFICATION_INITIATED",{environment:this.config.environment,mode:this.config.mode,sessionId:i?i.substring(0,8)+"...":"undefined",origin:window.location.origin}),this.config.mode==="new-tab"?this.openNewTab(g,i):(this.unlockVerification(),this.redirect(g))}catch(d){throw this.unlockVerification(),d}}async buildVerificationUrl(e,n){let i=M(this.config.environment),r=e.challengeAge!==void 0,s=e.verificationMode!==void 0,o=r||s,a=await ee({merchantId:this.config.apiKey,sessionId:n,returnUrl:this.config.returnUrl,cancelUrl:this.config.cancelUrl,challengeAge:e.challengeAge||this.config.defaultChallengeAge,verificationMode:e.verificationMode||this.config.defaultVerificationMode,hasOverrides:o,externalUserId:e.externalUserId,timestamp:Date.now(),apiUrl:this.getPortalApiUrl(),engineUrl:this.getEngineUrl(),wsUrl:this.getWebSocketUrl(),environment:this.config.environment,features:{testMode:!1,warmupPeriodMs:500,qualityThreshold:.6},handoffToken:this._temporaryHandoffToken,sessionToken:this.lastSessionToken||void 0,verifyUrl:this.lastVerifyUrl||void 0},this.config.environment);if(this.lastVerifyUrl)try{let c=new URL(this.lastVerifyUrl);return c.searchParams.set("state",a),c.searchParams.set("mode",this.config.mode),e.skipIntro&&c.searchParams.set("skip_intro","true"),e.autoReturn&&c.searchParams.set("auto_return","true"),c.toString()}catch(c){}let p=new URLSearchParams({state:a,sessionId:n,mode:this.config.mode});return e.skipIntro&&p.set("skip_intro","true"),e.autoReturn&&p.set("auto_return","true"),`${i}/?${p.toString()}`}redirect(e){window.location.href=e}openNewTab(e,n){var i,r;if(this.cleanup(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),this.popupWindow=window.open(e,"safepassage-verify","width=600,height=700"),!this.popupWindow){(r=(i=this.config).onError)==null||r.call(i,new Error("Failed to open verification window. Please check popup blocker settings."));return}this.messageListener=s=>{var p,c,d,g,f,I,m,L;if(!W(s,this.config.environment)){l("POSTMESSAGE_ORIGIN_BLOCKED",{origin:s.origin,environment:this.config.environment,expectedOrigins:`SafePassage trusted origins for ${this.config.environment}`,messageType:(p=s.data)==null?void 0:p.type});return}let o=H(s,n);if(!o.isValid){l("POSTMESSAGE_VALIDATION_FAILED",{error:o.error,origin:s.origin,sessionId:n.substring(0,8)+"...",messageType:(c=s.data)==null?void 0:c.type});return}let a={sessionId:s.data.sessionId,status:s.data.status};l("VERIFICATION_COMPLETED",{status:a.status,sessionId:n.substring(0,8)+"...",origin:s.origin}),this.cleanup(),this.unlockVerification(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),a.status==="verified"?(g=(d=this.config).onComplete)==null||g.call(d,a):a.status==="cancelled"?(I=(f=this.config).onCancel)==null||I.call(f):(L=(m=this.config).onError)==null||L.call(m,new Error(`Verification failed: ${a.status}`))},window.addEventListener("message",this.messageListener),this.popupMonitorInterval=setInterval(()=>{var s,o;this.popupWindow&&this.popupWindow.closed&&(l("POPUP_CLOSED_BY_USER",{sessionId:n.substring(0,8)+"...",environment:this.config.environment}),this.cleanup(),this.unlockVerification(),(o=(s=this.config).onCancel)==null||o.call(s))},500)}setupAutoCleanup(){if(this.unloadListener=()=>{l("SDK_AUTO_CLEANUP",{environment:this.config.environment,trigger:"page_unload"}),this.cleanup(),this.unlockVerification()},window.addEventListener("beforeunload",this.unloadListener),window.addEventListener("pagehide",this.unloadListener),window.history&&window.history.pushState){let e=window.history.pushState;window.history.pushState=(...n)=>(this.cleanup(),this.unlockVerification(),e.apply(window.history,n))}}detectEnvironment(){let e=window.location.hostname;return e.includes("staging")||e.includes("stage")?"staging":"production"}getEnvironment(){return this.config.environment}unlockVerification(){this.isVerificationInProgress=!1,this.currentSessionId=null,l("VERIFICATION_UNLOCKED",{environment:this.config.environment,origin:window.location.origin})}cleanup(){this.popupWindow&&!this.popupWindow.closed&&this.popupWindow.close(),this.popupWindow=null,this.messageListener&&(window.removeEventListener("message",this.messageListener),this.messageListener=null),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null)}removeAutoCleanupListeners(){this.unloadListener&&(window.removeEventListener("beforeunload",this.unloadListener),window.removeEventListener("pagehide",this.unloadListener),this.unloadListener=null)}destroy(){l("SDK_DESTROYED",{environment:this.config.environment,origin:window.location.origin}),this.cleanup(),this.unlockVerification(),this.removeAutoCleanupListeners()}getPortalApiUrl(){switch(this.config.environment){case"staging":return"https://api.staging.safepassageapp.com";case"production":return"https://api.safepassageapp.com";default:return"https://api.safepassageapp.com"}}getEngineUrl(){switch(this.config.environment){case"staging":return"https://engine.staging.safepassageapp.com";case"production":return"https://engine.safepassageapp.com";default:return"https://engine.safepassageapp.com"}}getWebSocketUrl(){switch(this.config.environment){case"staging":return"wss://engine.staging.safepassageapp.com/api/websocket/stream";case"production":return"wss://engine.safepassageapp.com/api/websocket/stream";default:return"wss://engine.safepassageapp.com/api/websocket/stream"}}isPublicKey(){return this.config.apiKey.startsWith("pk_")}async createInternalSession(e){var n,i;try{let r=this.getPortalApiUrl(),s=await fetch(`${r}/api/v1/sessions/create`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({merchantId:this.config.apiKey,returnUrl:this.config.returnUrl,cancelUrl:this.config.cancelUrl,challengeAge:e.challengeAge,verificationMode:e.verificationMode,merchantName:document.title||window.location.hostname,externalUserId:e.externalUserId})});if(!s.ok){let p=await s.json().catch(()=>({}));throw new Error(`Failed to create session: ${s.status} ${s.statusText}. ${p.message||""}`)}let o=await s.json(),a=o.sessionId;if(!a)throw new Error("Server did not return a sessionId");return o.verifyUrl&&(this.lastVerifyUrl=o.verifyUrl),o.sessionToken&&(this.lastSessionToken=o.sessionToken),o.handoffToken&&(this._temporaryHandoffToken=o.handoffToken),l("INTERNAL_SESSION_CREATED",{sessionId:a.substring(0,8)+"...",environment:this.config.environment,apiKeyType:"public"}),a}catch(r){let s=r instanceof Error?r.message:String(r);throw l("INTERNAL_SESSION_FAILED",{error:s,environment:this.config.environment,apiKeyType:"public"}),(i=(n=this.config).onError)==null||i.call(n,r),new Error(`Failed to create verification session: ${s}`)}}},ve=u});var Se={};U(Se,{SafePassage:()=>u,VERSION:()=>se,default:()=>u});function O(){crypto.randomUUID||(crypto.randomUUID=function(){let t=new Uint8Array(16);crypto.getRandomValues(t),t[6]=t[6]&15|64,t[8]=t[8]&63|128;let e=Array.from(t).map(n=>n.toString(16).padStart(2,"0")).join("");return[e.slice(0,8),e.slice(8,12),e.slice(12,16),e.slice(16,20),e.slice(20,32)].join("-")})}function N(){let t=[];if(!window.crypto||!window.crypto.getRandomValues)throw new Error("SafePassage SDK requires Web Crypto API support");if(!window.crypto.subtle)throw new Error("SafePassage SDK requires Web Crypto subtle API for HMAC operations");crypto.randomUUID||t.push("crypto.randomUUID not supported, using polyfill"),window.URLSearchParams||t.push("URLSearchParams not supported, consider adding a polyfill for IE 11 support"),t.length>0&&console.warn("SafePassage SDK Browser Compatibility:",t.join("; "))}_();typeof window!="undefined"&&(O(),N());var se="3.4.1";if(typeof window!="undefined"&&window){let{SafePassage:t}=(_(),$(re)),e=window;e.SafePassage=t,e.SafePassage&&(e.SafePassage.VERSION=se)}return $(Se);})();
|
|
1
|
+
/* SafePassage SDK v3.4.2 */
|
|
2
|
+
"use strict";var SafePassageSDK=(()=>{var v=Object.defineProperty,se=Object.defineProperties,oe=Object.getOwnPropertyDescriptor,ae=Object.getOwnPropertyDescriptors,ce=Object.getOwnPropertyNames,w=Object.getOwnPropertySymbols;var I=Object.prototype.hasOwnProperty,R=Object.prototype.propertyIsEnumerable;var L=(t,e,n)=>e in t?v(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,h=(t,e)=>{for(var n in e||(e={}))I.call(e,n)&&L(t,n,e[n]);if(w)for(var n of w(e))R.call(e,n)&&L(t,n,e[n]);return t},S=(t,e)=>se(t,ae(e));var C=(t,e)=>{var n={};for(var i in t)I.call(t,i)&&e.indexOf(i)<0&&(n[i]=t[i]);if(t!=null&&w)for(var i of w(t))e.indexOf(i)<0&&R.call(t,i)&&(n[i]=t[i]);return n};var m=(t,e)=>()=>(t&&(e=t(t=0)),e);var P=(t,e)=>{for(var n in e)v(t,n,{get:e[n],enumerable:!0})},le=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of ce(e))!I.call(t,r)&&r!==n&&v(t,r,{get:()=>e[r],enumerable:!(i=oe(e,r))||i.enumerable});return t};var D=t=>le(v({},"__esModule",{value:!0}),t);function pe(t,e){return N[e].includes(t)}function K(t,e,n=[]){var r;let{origin:i}=t;return pe(i,e)||n.length>0&&n.some(o=>{if(o.startsWith("*.")){let a=o.slice(2);return i.endsWith(`.${a}`)||i===`https://${a}`||i===`http://${a}`}return i===o})?!0:(console.warn(`SafePassage Security: Blocked PostMessage from untrusted origin: ${i}`,{environment:e,trustedOrigins:N[e],allowedCustomOrigins:n,eventType:(r=t.data)==null?void 0:r.type}),!1)}function W(t,e){let{data:n}=t;return!n||typeof n!="object"?{isValid:!1,error:"Invalid message format"}:n.type!=="safepassage:verification:complete"?{isValid:!1,error:"Invalid message type"}:!n.sessionId||n.sessionId!==e?{isValid:!1,error:"Session ID mismatch"}:!n.status||!["verified","failed"].includes(n.status)?{isValid:!1,error:"Invalid status value"}:{isValid:!0}}function H(t){t==="production"&&window.location.protocol!=="https:"&&console.warn("SafePassage Warning: HTTPS recommended for production environment",{current:window.location.href})}function A(t,e){try{let n=new URL(t);if(n.protocol!=="https:"&&!(n.hostname==="localhost"||n.hostname==="127.0.0.1"))return{isValid:!1,error:`HTTPS required for return URLs in ${e}`};let i=[/data:/i,/javascript:/i,/vbscript:/i,/file:/i,/ftp:/i];for(let r of i)if(r.test(t))return{isValid:!1,error:"Blocked suspicious URL scheme"};return{isValid:!0}}catch(n){return{isValid:!1,error:"Invalid URL format"}}}function l(t,e){console.warn(`SafePassage Security Event: ${t}`,h({timestamp:new Date().toISOString(),userAgent:navigator.userAgent,url:window.location.href},e))}var N,U,j,T=m(()=>{"use strict";N={production:["https://av.safepassageapp.com","https://portal.safepassageapp.com","https://api.safepassageapp.com"],staging:["https://av.staging.safepassageapp.com","https://portal.staging.safepassageapp.com","https://api.staging.safepassageapp.com"]};U=class{constructor(){this.attempts=new Map;this.maxAttempts=5;this.timeWindow=6e4}isAllowed(e){let n=Date.now(),r=(this.attempts.get(e)||[]).filter(s=>n-s<this.timeWindow);return r.length>=this.maxAttempts?(console.warn(`SafePassage Security: Rate limit exceeded for ${e}`),!1):(r.push(n),this.attempts.set(e,r),!0)}reset(e){this.attempts.delete(e)}},j=new U});var B={};P(B,{createSignedState:()=>ue,generateHMAC:()=>b,generateSecureToken:()=>F,getSigningSecret:()=>x,parseSignedState:()=>ge,verifyHMAC:()=>q});async function b(t,e){let n=new TextEncoder,i=n.encode(e),r=n.encode(t),s=await crypto.subtle.importKey("raw",i,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),o=await crypto.subtle.sign("HMAC",s,r);return Array.from(new Uint8Array(o)).map(a=>a.toString(16).padStart(2,"0")).join("")}async function q(t,e,n){try{let i=await b(t,n);return de(e,i)}catch(i){return!1}}function de(t,e){if(t.length!==e.length)return!1;let n=0;for(let i=0;i<t.length;i++)n|=t.charCodeAt(i)^e.charCodeAt(i);return n===0}function F(t=32){let e=new Uint8Array(t);return crypto.getRandomValues(e),Array.from(e,n=>n.toString(16).padStart(2,"0")).join("")}function x(t){return{production:"safepassage-prod-hmac-2025",staging:"safepassage-stage-hmac-2025"}[t]}async function ue(t,e){let n=S(h({},t),{timestamp:Date.now(),nonce:F(16)}),i=JSON.stringify(n),r=x(e),s=await b(i,r);return btoa(JSON.stringify({data:n,signature:s}))}async function ge(t,e,n=J){try{let r=atob(t),s=JSON.parse(r);if(!s.data||!s.signature)return console.warn("SafePassage: Invalid signed state format"),null;let{data:o,signature:a}=s,p=JSON.stringify(o),c=x(e);if(!await q(p,a,c))return console.warn("SafePassage: State signature verification failed"),null;if(o.timestamp){let _=Date.now()-o.timestamp;if(_>n)return console.warn("SafePassage: State parameter expired",{age:_,maxAge:n}),null}let i=o,{timestamp:g,nonce:f}=i;return C(i,["timestamp","nonce"])}catch(r){return console.warn("SafePassage: Failed to parse signed state",r),null}}var G=m(()=>{"use strict";V()});function Z(t){if(!t.apiKey)throw new Error("apiKey is required");if(t.apiKey.length>z)throw new Error(`apiKey exceeds maximum length of ${z} characters`);if(!fe.test(t.apiKey))throw new Error("Invalid apiKey format. Expected pk_xxx (public) or sk_xxx (private)");if(typeof window!="undefined"&&t.apiKey.startsWith("sk_"))throw new Error("Secret keys (sk_) should never be used in browser code for security reasons. Secret keys expose your account to unauthorized access if used client-side. Please use your public key (pk_) instead. If you need to use features that require a secret key (like custom challenge age), create the session server-side and pass the sessionId to startVerificationWithSession(). See: https://docs.safepassageapp.com/server-side-sessions");if(!t.returnUrl)throw new Error("returnUrl is required");if(t.returnUrl.length>y)throw new Error(`returnUrl exceeds maximum length of ${y} characters`);let e=he(),n=A(t.returnUrl,e);if(!n.isValid)throw new Error(`returnUrl validation failed: ${n.error}`);if(t.cancelUrl){if(t.cancelUrl.length>y)throw new Error(`cancelUrl exceeds maximum length of ${y} characters`);let i=A(t.cancelUrl,e);if(!i.isValid)throw new Error(`cancelUrl validation failed: ${i.error}`)}if(t.defaultChallengeAge!==void 0){if(t.defaultChallengeAge<X)throw new Error(`defaultChallengeAge must be at least ${X}`);if(t.defaultChallengeAge>Y)throw new Error(`defaultChallengeAge cannot exceed ${Y}`)}if(t.defaultVerificationMode&&!["L1","L2"].includes(t.defaultVerificationMode))throw new Error("defaultVerificationMode must be L1 or L2");if(t.mode&&!["redirect","new-tab"].includes(t.mode))throw new Error("mode must be redirect or new-tab")}function he(){if(typeof window=="undefined")return"production";let t=window.location.hostname;return t.includes("staging")||t.includes("stage")?"staging":"production"}async function Q(t,e){let{createSignedState:n}=await Promise.resolve().then(()=>(G(),B));return n(t,e)}var X,Y,y,z,J,fe,V=m(()=>{"use strict";T();X=25,Y=150,y=2048,z=128,J=6e5,fe=/^(pk_|sk_)[a-zA-Z0-9_]+$/});function k(t){let e=ee[t]||ee.production;if(!e||!e.startsWith("https://"))throw new Error(`HTTPS required for ${t} environment`);return e}function me(t){let e={production:"https://api.safepassageapp.com",staging:"https://api.staging.safepassageapp.com"},n=e[t]||e.production;if(!n||!n.startsWith("https://"))throw new Error(`HTTPS required for API URLs in ${t} environment`);return n}function te(t){let e=window.location.protocol==="https:";switch(t){case"production":e||console.warn("SafePassage Warning: HTTPS recommended for production environment");break;case"staging":e||console.warn("SafePassage Warning: HTTPS strongly recommended in staging environment");break}try{k(t),me(t)}catch(n){let i=n instanceof Error?n.message:String(n);throw new Error(`Environment configuration validation failed: ${i}`)}}var ee,ne=m(()=>{"use strict";ee={production:"https://av.safepassageapp.com",staging:"https://av.staging.safepassageapp.com"}});var ie={};P(ie,{SafePassage:()=>u,default:()=>we});var u,we,M=m(()=>{"use strict";V();ne();T();u=class{constructor(e){this.popupWindow=null;this.messageListener=null;this.popupMonitorInterval=null;this.unloadListener=null;this.isVerificationInProgress=!1;this.currentSessionId=null;this.lastVerifyUrl=null;this.lastSessionToken=null;Z(e);let n=e.environment||this.detectEnvironment();n!=="staging"&&n!=="production"&&(console.warn(`SafePassage SDK: Unknown environment '${n}', defaulting to 'production'`),n="production"),this.config=S(h({},e),{environment:n,mode:e.mode||"redirect"}),te(this.config.environment),H(this.config.environment),l("SDK_INITIALIZED",{environment:this.config.environment,mode:this.config.mode,origin:window.location.origin,protocol:window.location.protocol,hostname:window.location.hostname}),this.setupAutoCleanup()}async verify(e={}){var r,s,o,a,p,c;let n=this.isPublicKey(),i;if(n)i=await this.createInternalSession(e);else throw new Error("Private API keys (sk_) should use the direct API, not the SDK. The SDK is designed for browser-based public key usage only.");if(!i)throw new Error("Failed to obtain sessionId from server");if(this.isVerificationInProgress){let d=new Error(`Verification already in progress for session ${(r=this.currentSessionId)==null?void 0:r.substring(0,8)}...`);throw l("RACE_CONDITION_PREVENTED",{currentSession:((s=this.currentSessionId)==null?void 0:s.substring(0,8))+"...",attemptedSession:"new-session-attempt",origin:window.location.origin}),(a=(o=this.config).onError)==null||a.call(o,d),d}this.isVerificationInProgress=!0,this.currentSessionId=i;try{let d=`${this.config.apiKey}:${window.location.origin}`;if(!j.isAllowed(d)){let f=new Error("Too many verification attempts. Please wait before trying again.");throw l("RATE_LIMIT_EXCEEDED",{apiKey:this.config.apiKey.substring(0,8)+"...",origin:window.location.origin,sessionId:i?i.substring(0,8)+"...":"undefined"}),(c=(p=this.config).onError)==null||c.call(p,f),f}let g=await this.buildVerificationUrl(e,i);l("VERIFICATION_INITIATED",{environment:this.config.environment,mode:this.config.mode,sessionId:i?i.substring(0,8)+"...":"undefined",origin:window.location.origin}),this.config.mode==="new-tab"?this.openNewTab(g,i):(this.unlockVerification(),this.redirect(g))}catch(d){throw this.unlockVerification(),d}}async buildVerificationUrl(e,n){let i=k(this.config.environment),r=e.challengeAge!==void 0,s=e.verificationMode!==void 0,o=r||s,a=await Q({merchantId:this.config.apiKey,sessionId:n,returnUrl:this.config.returnUrl,cancelUrl:this.config.cancelUrl,challengeAge:e.challengeAge||this.config.defaultChallengeAge,verificationMode:e.verificationMode||this.config.defaultVerificationMode,hasOverrides:o,externalUserId:e.externalUserId,timestamp:Date.now(),apiUrl:this.getPortalApiUrl(),engineUrl:this.getEngineUrl(),wsUrl:this.getWebSocketUrl(),environment:this.config.environment,features:{testMode:!1,warmupPeriodMs:500,qualityThreshold:.6},handoffToken:this._temporaryHandoffToken,sessionToken:this.lastSessionToken||void 0,verifyUrl:this.lastVerifyUrl||void 0},this.config.environment);if(this.lastVerifyUrl)try{let c=new URL(this.lastVerifyUrl);return c.searchParams.set("state",a),c.searchParams.set("mode",this.config.mode),e.skipIntro&&c.searchParams.set("skip_intro","true"),e.autoReturn&&c.searchParams.set("auto_return","true"),c.toString()}catch(c){}let p=new URLSearchParams({state:a,sessionId:n,mode:this.config.mode});return e.skipIntro&&p.set("skip_intro","true"),e.autoReturn&&p.set("auto_return","true"),`${i}/?${p.toString()}`}redirect(e){window.location.href=e}openNewTab(e,n){var i,r;if(this.cleanup(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),this.popupWindow=window.open(e,"safepassage-verify","width=600,height=700"),!this.popupWindow){(r=(i=this.config).onError)==null||r.call(i,new Error("Failed to open verification window. Please check popup blocker settings."));return}this.messageListener=s=>{var p,c,d,g,f,E;if(!K(s,this.config.environment)){l("POSTMESSAGE_ORIGIN_BLOCKED",{origin:s.origin,environment:this.config.environment,expectedOrigins:`SafePassage trusted origins for ${this.config.environment}`,messageType:(p=s.data)==null?void 0:p.type});return}let o=W(s,n);if(!o.isValid){l("POSTMESSAGE_VALIDATION_FAILED",{error:o.error,origin:s.origin,sessionId:n.substring(0,8)+"...",messageType:(c=s.data)==null?void 0:c.type});return}let a={sessionId:s.data.sessionId,status:s.data.status};l("VERIFICATION_COMPLETED",{status:a.status,sessionId:n.substring(0,8)+"...",origin:s.origin}),this.cleanup(),this.unlockVerification(),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null),a.status==="verified"?(g=(d=this.config).onComplete)==null||g.call(d,a):(E=(f=this.config).onError)==null||E.call(f,new Error(`Verification failed: ${a.status}`))},window.addEventListener("message",this.messageListener),this.popupMonitorInterval=setInterval(()=>{var s,o;this.popupWindow&&this.popupWindow.closed&&(l("POPUP_CLOSED_BY_USER",{sessionId:n.substring(0,8)+"...",environment:this.config.environment}),this.cleanup(),this.unlockVerification(),(o=(s=this.config).onCancel)==null||o.call(s))},500)}setupAutoCleanup(){if(this.unloadListener=()=>{l("SDK_AUTO_CLEANUP",{environment:this.config.environment,trigger:"page_unload"}),this.cleanup(),this.unlockVerification()},window.addEventListener("beforeunload",this.unloadListener),window.addEventListener("pagehide",this.unloadListener),window.history&&window.history.pushState){let e=window.history.pushState;window.history.pushState=(...n)=>(this.cleanup(),this.unlockVerification(),e.apply(window.history,n))}}detectEnvironment(){let e=window.location.hostname;return e.includes("staging")||e.includes("stage")?"staging":"production"}getEnvironment(){return this.config.environment}unlockVerification(){this.isVerificationInProgress=!1,this.currentSessionId=null,l("VERIFICATION_UNLOCKED",{environment:this.config.environment,origin:window.location.origin})}cleanup(){this.popupWindow&&!this.popupWindow.closed&&this.popupWindow.close(),this.popupWindow=null,this.messageListener&&(window.removeEventListener("message",this.messageListener),this.messageListener=null),this.popupMonitorInterval&&(clearInterval(this.popupMonitorInterval),this.popupMonitorInterval=null)}removeAutoCleanupListeners(){this.unloadListener&&(window.removeEventListener("beforeunload",this.unloadListener),window.removeEventListener("pagehide",this.unloadListener),this.unloadListener=null)}destroy(){l("SDK_DESTROYED",{environment:this.config.environment,origin:window.location.origin}),this.cleanup(),this.unlockVerification(),this.removeAutoCleanupListeners()}getPortalApiUrl(){switch(this.config.environment){case"staging":return"https://api.staging.safepassageapp.com";case"production":return"https://api.safepassageapp.com";default:return"https://api.safepassageapp.com"}}getEngineUrl(){switch(this.config.environment){case"staging":return"https://engine.staging.safepassageapp.com";case"production":return"https://engine.safepassageapp.com";default:return"https://engine.safepassageapp.com"}}getWebSocketUrl(){switch(this.config.environment){case"staging":return"wss://engine.staging.safepassageapp.com/api/websocket/stream";case"production":return"wss://engine.safepassageapp.com/api/websocket/stream";default:return"wss://engine.safepassageapp.com/api/websocket/stream"}}isPublicKey(){return this.config.apiKey.startsWith("pk_")}async createInternalSession(e){var n,i;try{let r=this.getPortalApiUrl(),s=await fetch(`${r}/api/v1/sessions/create`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({merchantId:this.config.apiKey,returnUrl:this.config.returnUrl,cancelUrl:this.config.cancelUrl,challengeAge:e.challengeAge,verificationMode:e.verificationMode,merchantName:document.title||window.location.hostname,externalUserId:e.externalUserId})});if(!s.ok){let p=await s.json().catch(()=>({}));throw new Error(`Failed to create session: ${s.status} ${s.statusText}. ${p.message||""}`)}let o=await s.json(),a=o.sessionId;if(!a)throw new Error("Server did not return a sessionId");return o.verifyUrl&&(this.lastVerifyUrl=o.verifyUrl),o.sessionToken&&(this.lastSessionToken=o.sessionToken),o.handoffToken&&(this._temporaryHandoffToken=o.handoffToken),l("INTERNAL_SESSION_CREATED",{sessionId:a.substring(0,8)+"...",environment:this.config.environment,apiKeyType:"public"}),a}catch(r){let s=r instanceof Error?r.message:String(r);throw l("INTERNAL_SESSION_FAILED",{error:s,environment:this.config.environment,apiKeyType:"public"}),(i=(n=this.config).onError)==null||i.call(n,r),new Error(`Failed to create verification session: ${s}`)}}},we=u});var ve={};P(ve,{SafePassage:()=>u,VERSION:()=>re,default:()=>u});function $(){crypto.randomUUID||(crypto.randomUUID=function(){let t=new Uint8Array(16);crypto.getRandomValues(t),t[6]=t[6]&15|64,t[8]=t[8]&63|128;let e=Array.from(t).map(n=>n.toString(16).padStart(2,"0")).join("");return[e.slice(0,8),e.slice(8,12),e.slice(12,16),e.slice(16,20),e.slice(20,32)].join("-")})}function O(){let t=[];if(!window.crypto||!window.crypto.getRandomValues)throw new Error("SafePassage SDK requires Web Crypto API support");if(!window.crypto.subtle)throw new Error("SafePassage SDK requires Web Crypto subtle API for HMAC operations");crypto.randomUUID||t.push("crypto.randomUUID not supported, using polyfill"),window.URLSearchParams||t.push("URLSearchParams not supported, consider adding a polyfill for IE 11 support"),t.length>0&&console.warn("SafePassage SDK Browser Compatibility:",t.join("; "))}M();typeof window!="undefined"&&($(),O());var re="3.4.2";if(typeof window!="undefined"&&window){let{SafePassage:t}=(M(),D(ie)),e=window;e.SafePassage=t,e.SafePassage&&(e.SafePassage.VERSION=re)}return D(ve);})();
|
|
3
3
|
if(typeof SafePassageSDK !== "undefined" && SafePassageSDK.SafePassage) { window.SafePassage = SafePassageSDK.SafePassage; window.SafePassage.VERSION = SafePassageSDK.VERSION; }
|
package/dist/types/index.d.ts
CHANGED
|
@@ -91,7 +91,7 @@ export interface VerificationResult {
|
|
|
91
91
|
* Binary result: 'verified' or 'failed'
|
|
92
92
|
* Full details available via server-side API
|
|
93
93
|
*/
|
|
94
|
-
status: 'verified' | 'failed'
|
|
94
|
+
status: 'verified' | 'failed';
|
|
95
95
|
/**
|
|
96
96
|
* External user identifier if provided during verification
|
|
97
97
|
*/
|
package/dist/utils/security.js
CHANGED
|
@@ -78,8 +78,7 @@ export function validateSafePassageMessage(event, expectedSessionId) {
|
|
|
78
78
|
return { isValid: false, error: 'Session ID mismatch' };
|
|
79
79
|
}
|
|
80
80
|
// Check status field
|
|
81
|
-
if (!data.status ||
|
|
82
|
-
!['verified', 'failed', 'cancelled'].includes(data.status)) {
|
|
81
|
+
if (!data.status || !['verified', 'failed'].includes(data.status)) {
|
|
83
82
|
return { isValid: false, error: 'Invalid status value' };
|
|
84
83
|
}
|
|
85
84
|
return { isValid: true };
|