@safepassage/sdk 3.0.4 → 3.0.6
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 +2 -7
- package/dist/core/SafePassageSDK.d.ts +158 -5
- package/dist/core/SafePassageSDK.js +237 -42
- package/dist/index.d.ts +19 -115
- package/dist/index.js +9 -3
- package/dist/safepassage.min.js +2 -2
- package/dist/types/index.d.ts +10 -0
- package/dist/utils/__mocks__/polyfills.d.ts +3 -0
- package/dist/utils/__mocks__/polyfills.js +10 -0
- package/dist/utils/crypto.d.ts +80 -6
- package/dist/utils/crypto.js +92 -10
- package/dist/utils/environment.js +19 -11
- package/dist/utils/polyfills.js +20 -17
- package/dist/utils/security.d.ts +1 -1
- package/dist/utils/security.js +33 -21
- package/dist/utils/validation.js +20 -7
- package/package.json +17 -13
package/dist/utils/security.js
CHANGED
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
const TRUSTED_ORIGINS = {
|
|
10
10
|
production: [
|
|
11
|
-
'https://
|
|
11
|
+
'https://av.safepassageapp.com',
|
|
12
12
|
'https://portal.safepassageapp.com',
|
|
13
|
-
'https://api.safepassageapp.com'
|
|
13
|
+
'https://api.safepassageapp.com',
|
|
14
14
|
],
|
|
15
15
|
staging: [
|
|
16
|
-
'https://
|
|
17
|
-
'https://portal
|
|
18
|
-
'https://api
|
|
16
|
+
'https://av.safepassageapp.com',
|
|
17
|
+
'https://portal.safepassageapp.com',
|
|
18
|
+
'https://api.safepassageapp.com',
|
|
19
19
|
],
|
|
20
20
|
development: [
|
|
21
21
|
'http://localhost:5173',
|
|
@@ -25,8 +25,8 @@ const TRUSTED_ORIGINS = {
|
|
|
25
25
|
'http://127.0.0.1:5173',
|
|
26
26
|
'http://127.0.0.1:3000',
|
|
27
27
|
'http://127.0.0.1:3001',
|
|
28
|
-
'http://127.0.0.1:3002'
|
|
29
|
-
]
|
|
28
|
+
'http://127.0.0.1:3002',
|
|
29
|
+
],
|
|
30
30
|
};
|
|
31
31
|
/**
|
|
32
32
|
* Validate if an origin is trusted for the given environment
|
|
@@ -46,11 +46,13 @@ export function validatePostMessageOrigin(event, environment, allowedCustomOrigi
|
|
|
46
46
|
}
|
|
47
47
|
// Check against custom allowed origins (for merchant websites)
|
|
48
48
|
if (allowedCustomOrigins.length > 0) {
|
|
49
|
-
const isCustomOriginAllowed = allowedCustomOrigins.some(allowedOrigin => {
|
|
49
|
+
const isCustomOriginAllowed = allowedCustomOrigins.some((allowedOrigin) => {
|
|
50
50
|
// Support wildcard subdomains (e.g., "*.example.com")
|
|
51
51
|
if (allowedOrigin.startsWith('*.')) {
|
|
52
52
|
const domain = allowedOrigin.slice(2);
|
|
53
|
-
return origin.endsWith(`.${domain}`) ||
|
|
53
|
+
return (origin.endsWith(`.${domain}`) ||
|
|
54
|
+
origin === `https://${domain}` ||
|
|
55
|
+
origin === `http://${domain}`);
|
|
54
56
|
}
|
|
55
57
|
return origin === allowedOrigin;
|
|
56
58
|
});
|
|
@@ -63,7 +65,7 @@ export function validatePostMessageOrigin(event, environment, allowedCustomOrigi
|
|
|
63
65
|
environment,
|
|
64
66
|
trustedOrigins: TRUSTED_ORIGINS[environment],
|
|
65
67
|
allowedCustomOrigins,
|
|
66
|
-
eventType: event.data?.type
|
|
68
|
+
eventType: event.data?.type,
|
|
67
69
|
});
|
|
68
70
|
return false;
|
|
69
71
|
}
|
|
@@ -85,7 +87,8 @@ export function validateSafePassageMessage(event, expectedSessionId) {
|
|
|
85
87
|
return { isValid: false, error: 'Session ID mismatch' };
|
|
86
88
|
}
|
|
87
89
|
// Check status field
|
|
88
|
-
if (!data.status ||
|
|
90
|
+
if (!data.status ||
|
|
91
|
+
!['verified', 'failed', 'cancelled'].includes(data.status)) {
|
|
89
92
|
return { isValid: false, error: 'Invalid status value' };
|
|
90
93
|
}
|
|
91
94
|
return { isValid: true };
|
|
@@ -98,7 +101,7 @@ export function enforceHTTPS(environment) {
|
|
|
98
101
|
const httpsUrl = window.location.href.replace('http:', 'https:');
|
|
99
102
|
console.error('SafePassage Security: HTTPS required in production. Redirecting...', {
|
|
100
103
|
current: window.location.href,
|
|
101
|
-
redirect: httpsUrl
|
|
104
|
+
redirect: httpsUrl,
|
|
102
105
|
});
|
|
103
106
|
window.location.replace(httpsUrl);
|
|
104
107
|
}
|
|
@@ -111,7 +114,10 @@ export function validateReturnUrl(url, environment) {
|
|
|
111
114
|
const parsed = new URL(url);
|
|
112
115
|
// Production must use HTTPS
|
|
113
116
|
if (environment === 'production' && parsed.protocol !== 'https:') {
|
|
114
|
-
return {
|
|
117
|
+
return {
|
|
118
|
+
isValid: false,
|
|
119
|
+
error: 'HTTPS required for return URLs in production',
|
|
120
|
+
};
|
|
115
121
|
}
|
|
116
122
|
// Development allows HTTP localhost
|
|
117
123
|
if (environment === 'development') {
|
|
@@ -124,7 +130,10 @@ export function validateReturnUrl(url, environment) {
|
|
|
124
130
|
}
|
|
125
131
|
// Staging should use HTTPS
|
|
126
132
|
if (environment === 'staging' && parsed.protocol !== 'https:') {
|
|
127
|
-
return {
|
|
133
|
+
return {
|
|
134
|
+
isValid: false,
|
|
135
|
+
error: 'HTTPS required for return URLs in staging',
|
|
136
|
+
};
|
|
128
137
|
}
|
|
129
138
|
// Block suspicious URLs
|
|
130
139
|
const suspiciousPatterns = [
|
|
@@ -132,7 +141,7 @@ export function validateReturnUrl(url, environment) {
|
|
|
132
141
|
/javascript:/i,
|
|
133
142
|
/vbscript:/i,
|
|
134
143
|
/file:/i,
|
|
135
|
-
/ftp:/i
|
|
144
|
+
/ftp:/i,
|
|
136
145
|
];
|
|
137
146
|
for (const pattern of suspiciousPatterns) {
|
|
138
147
|
if (pattern.test(url)) {
|
|
@@ -141,7 +150,7 @@ export function validateReturnUrl(url, environment) {
|
|
|
141
150
|
}
|
|
142
151
|
return { isValid: true };
|
|
143
152
|
}
|
|
144
|
-
catch
|
|
153
|
+
catch {
|
|
145
154
|
return { isValid: false, error: 'Invalid URL format' };
|
|
146
155
|
}
|
|
147
156
|
}
|
|
@@ -157,13 +166,16 @@ export function generateSecureSessionId() {
|
|
|
157
166
|
const array = new Uint8Array(16);
|
|
158
167
|
crypto.getRandomValues(array);
|
|
159
168
|
// Convert to UUID v4 format
|
|
160
|
-
const hex = Array.from(array)
|
|
169
|
+
const hex = Array.from(array)
|
|
170
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
171
|
+
.join('');
|
|
161
172
|
return [
|
|
162
173
|
hex.slice(0, 8),
|
|
163
174
|
hex.slice(8, 12),
|
|
164
175
|
'4' + hex.slice(13, 16), // Version 4
|
|
165
|
-
((parseInt(hex.slice(16, 17), 16) & 0x3) | 0x8).toString(16) +
|
|
166
|
-
|
|
176
|
+
((parseInt(hex.slice(16, 17), 16) & 0x3) | 0x8).toString(16) +
|
|
177
|
+
hex.slice(17, 20), // Variant
|
|
178
|
+
hex.slice(20, 32),
|
|
167
179
|
].join('-');
|
|
168
180
|
}
|
|
169
181
|
/**
|
|
@@ -179,7 +191,7 @@ class VerificationRateLimit {
|
|
|
179
191
|
const now = Date.now();
|
|
180
192
|
const attempts = this.attempts.get(identifier) || [];
|
|
181
193
|
// Filter out old attempts
|
|
182
|
-
const recentAttempts = attempts.filter(time => now - time < this.timeWindow);
|
|
194
|
+
const recentAttempts = attempts.filter((time) => now - time < this.timeWindow);
|
|
183
195
|
if (recentAttempts.length >= this.maxAttempts) {
|
|
184
196
|
console.warn(`SafePassage Security: Rate limit exceeded for ${identifier}`);
|
|
185
197
|
return false;
|
|
@@ -202,7 +214,7 @@ export function logSecurityEvent(event, details) {
|
|
|
202
214
|
timestamp: new Date().toISOString(),
|
|
203
215
|
userAgent: navigator.userAgent,
|
|
204
216
|
url: window.location.href,
|
|
205
|
-
...details
|
|
217
|
+
...details,
|
|
206
218
|
});
|
|
207
219
|
// In production, this could send events to a security monitoring service
|
|
208
220
|
}
|
package/dist/utils/validation.js
CHANGED
|
@@ -59,7 +59,8 @@ export function validateConfig(config) {
|
|
|
59
59
|
throw new Error(`defaultChallengeAge cannot exceed ${MAXIMUM_AGE}`);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
if (config.defaultVerificationMode &&
|
|
62
|
+
if (config.defaultVerificationMode &&
|
|
63
|
+
!['L1', 'L2'].includes(config.defaultVerificationMode)) {
|
|
63
64
|
throw new Error('defaultVerificationMode must be L1 or L2');
|
|
64
65
|
}
|
|
65
66
|
if (config.mode && !['redirect', 'new-tab'].includes(config.mode)) {
|
|
@@ -71,7 +72,9 @@ export function validateConfig(config) {
|
|
|
71
72
|
*/
|
|
72
73
|
function detectEnvironment() {
|
|
73
74
|
const hostname = window.location.hostname;
|
|
74
|
-
if (hostname === 'localhost' ||
|
|
75
|
+
if (hostname === 'localhost' ||
|
|
76
|
+
hostname === '127.0.0.1' ||
|
|
77
|
+
hostname.includes('.local')) {
|
|
75
78
|
return 'development';
|
|
76
79
|
}
|
|
77
80
|
if (hostname.includes('staging') || hostname.includes('stage')) {
|
|
@@ -117,25 +120,35 @@ export async function parseState(state, environment) {
|
|
|
117
120
|
// Try parsing as signed state first (new format)
|
|
118
121
|
const signedPayload = await parseSignedState(state, environment);
|
|
119
122
|
if (signedPayload) {
|
|
123
|
+
// Type guard to ensure signedPayload has required properties
|
|
124
|
+
const payload = signedPayload;
|
|
120
125
|
// Validate payload structure
|
|
121
|
-
if (!
|
|
122
|
-
!
|
|
126
|
+
if (!payload.merchantId ||
|
|
127
|
+
!payload.sessionId ||
|
|
128
|
+
!payload.returnUrl ||
|
|
129
|
+
!payload.cancelUrl) {
|
|
123
130
|
return null;
|
|
124
131
|
}
|
|
125
|
-
return
|
|
132
|
+
return payload;
|
|
126
133
|
}
|
|
127
134
|
// Fallback to legacy base64 format for backwards compatibility
|
|
128
135
|
console.warn('SafePassage: Falling back to legacy state format - update your SDK');
|
|
129
136
|
const json = atob(state);
|
|
130
137
|
const payload = JSON.parse(json);
|
|
131
138
|
// Validate payload structure
|
|
132
|
-
if (!payload.merchantId ||
|
|
139
|
+
if (!payload.merchantId ||
|
|
140
|
+
!payload.sessionId ||
|
|
141
|
+
!payload.returnUrl ||
|
|
142
|
+
!payload.cancelUrl) {
|
|
133
143
|
return null;
|
|
134
144
|
}
|
|
135
145
|
// Check timestamp expiration
|
|
136
146
|
const age = Date.now() - payload.timestamp;
|
|
137
147
|
if (age > STATE_EXPIRY_MS) {
|
|
138
|
-
console.warn('SafePassage: State parameter expired', {
|
|
148
|
+
console.warn('SafePassage: State parameter expired', {
|
|
149
|
+
age,
|
|
150
|
+
maxAge: STATE_EXPIRY_MS,
|
|
151
|
+
});
|
|
139
152
|
return null;
|
|
140
153
|
}
|
|
141
154
|
return payload;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@safepassage/sdk",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.6",
|
|
4
4
|
"description": "SafePassage SDK - Lightweight redirect-based age verification",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -14,16 +14,16 @@
|
|
|
14
14
|
"dev": "tsc --watch",
|
|
15
15
|
"demo": "python3 -m http.server 8080",
|
|
16
16
|
"serve-demo": "npx serve . -p 8080",
|
|
17
|
-
"test": "jest
|
|
18
|
-
"test:browser": "jest
|
|
19
|
-
"test:mobile": "jest
|
|
20
|
-
"test:performance": "jest
|
|
21
|
-
"test:security": "jest
|
|
22
|
-
"test:accessibility": "jest
|
|
23
|
-
"test:edge": "jest
|
|
24
|
-
"test:integration": "jest
|
|
25
|
-
"test:all": "
|
|
26
|
-
"test:coverage": "jest --
|
|
17
|
+
"test": "jest",
|
|
18
|
+
"test:browser": "jest test/browser-compat",
|
|
19
|
+
"test:mobile": "jest test/mobile",
|
|
20
|
+
"test:performance": "jest test/performance",
|
|
21
|
+
"test:security": "jest test/security",
|
|
22
|
+
"test:accessibility": "jest test/accessibility",
|
|
23
|
+
"test:edge": "jest test/edge-cases",
|
|
24
|
+
"test:integration": "jest test/integration",
|
|
25
|
+
"test:all": "jest",
|
|
26
|
+
"test:coverage": "jest --coverage",
|
|
27
27
|
"benchmark": "node test/performance/benchmark.js",
|
|
28
28
|
"prepublishOnly": "npm run build",
|
|
29
29
|
"version": "npm run build && git add -A dist",
|
|
@@ -42,15 +42,19 @@
|
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@babel/core": "^7.27.4",
|
|
44
44
|
"@babel/preset-env": "^7.27.2",
|
|
45
|
-
"@
|
|
45
|
+
"@testing-library/dom": "^10.4.0",
|
|
46
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
47
|
+
"@testing-library/user-event": "^14.6.1",
|
|
48
|
+
"@types/jest": "^29.5.14",
|
|
46
49
|
"@types/node": "^24.0.1",
|
|
47
50
|
"babel-jest": "^30.0.0",
|
|
48
51
|
"esbuild": "^0.25.5",
|
|
49
52
|
"jest": "^29.7.0",
|
|
50
53
|
"jest-environment-jsdom": "^29.7.0",
|
|
54
|
+
"jsdom": "^26.1.0",
|
|
51
55
|
"puppeteer": "^21.11.0",
|
|
52
56
|
"terser": "^5.42.0",
|
|
53
|
-
"ts-jest": "^29.
|
|
57
|
+
"ts-jest": "^29.4.0",
|
|
54
58
|
"typescript": "~5.8.3"
|
|
55
59
|
},
|
|
56
60
|
"repository": {
|