@safepassage/sdk 3.4.9 → 3.4.11
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 +21 -14
- package/brands/safepassage/index.d.ts +12 -0
- package/brands/safepassage/urls.d.ts +3 -0
- package/core/VerificationSDK.d.ts +135 -0
- package/index.d.ts +2 -0
- package/index.js +1143 -0
- package/package.json +14 -47
- package/safepassage.min.js +3 -0
- package/sdk.min.js +3 -0
- package/{dist/types/index.d.ts → types/base.d.ts} +28 -10
- package/utils/crypto.d.ts +31 -0
- package/utils/environment.d.ts +8 -0
- package/{dist/utils → utils}/polyfills.d.ts +1 -2
- package/{dist/utils → utils}/security.d.ts +11 -11
- package/{dist/utils → utils}/validation.d.ts +9 -12
- package/dist/core/SafePassageSDK.d.ts +0 -231
- package/dist/core/SafePassageSDK.js +0 -656
- package/dist/index.d.ts +0 -21
- package/dist/index.js +0 -1100
- package/dist/safepassage.min.js +0 -3
- package/dist/types/index.js +0 -4
- package/dist/utils/__mocks__/polyfills.d.ts +0 -3
- package/dist/utils/__mocks__/polyfills.js +0 -10
- package/dist/utils/crypto.d.ts +0 -105
- package/dist/utils/crypto.js +0 -216
- package/dist/utils/environment.d.ts +0 -13
- package/dist/utils/environment.js +0 -75
- package/dist/utils/polyfills.js +0 -58
- package/dist/utils/security.js +0 -190
- package/dist/utils/validation.js +0 -158
package/dist/utils/security.js
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Security utilities for SafePassage SDK
|
|
3
|
-
* Enhanced origin validation and security enforcement
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Trusted SafePassage origins for different environments
|
|
7
|
-
* These are the only origins allowed to send PostMessage events
|
|
8
|
-
*/
|
|
9
|
-
const TRUSTED_ORIGINS = {
|
|
10
|
-
production: [
|
|
11
|
-
'https://av.safepassageapp.com',
|
|
12
|
-
'https://portal.safepassageapp.com',
|
|
13
|
-
'https://api.safepassageapp.com',
|
|
14
|
-
],
|
|
15
|
-
staging: [
|
|
16
|
-
'https://av.staging.safepassageapp.com',
|
|
17
|
-
'https://portal.staging.safepassageapp.com',
|
|
18
|
-
'https://api.staging.safepassageapp.com',
|
|
19
|
-
],
|
|
20
|
-
};
|
|
21
|
-
/**
|
|
22
|
-
* Validate if an origin is trusted for the given environment
|
|
23
|
-
*/
|
|
24
|
-
export function isOriginTrusted(origin, environment) {
|
|
25
|
-
const trustedOrigins = TRUSTED_ORIGINS[environment];
|
|
26
|
-
return trustedOrigins.includes(origin);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Enhanced origin validation with logging and strict allowlist
|
|
30
|
-
*/
|
|
31
|
-
export function validatePostMessageOrigin(event, environment, allowedCustomOrigins = []) {
|
|
32
|
-
var _a;
|
|
33
|
-
const { origin } = event;
|
|
34
|
-
// Check against trusted SafePassage origins
|
|
35
|
-
if (isOriginTrusted(origin, environment)) {
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
// Check against custom allowed origins (for merchant websites)
|
|
39
|
-
if (allowedCustomOrigins.length > 0) {
|
|
40
|
-
const isCustomOriginAllowed = allowedCustomOrigins.some((allowedOrigin) => {
|
|
41
|
-
// Support wildcard subdomains (e.g., "*.example.com")
|
|
42
|
-
if (allowedOrigin.startsWith('*.')) {
|
|
43
|
-
const domain = allowedOrigin.slice(2);
|
|
44
|
-
return (origin.endsWith(`.${domain}`) ||
|
|
45
|
-
origin === `https://${domain}` ||
|
|
46
|
-
origin === `http://${domain}`);
|
|
47
|
-
}
|
|
48
|
-
return origin === allowedOrigin;
|
|
49
|
-
});
|
|
50
|
-
if (isCustomOriginAllowed) {
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
// Log security violation for debugging
|
|
55
|
-
console.warn(`SafePassage Security: Blocked PostMessage from untrusted origin: ${origin}`, {
|
|
56
|
-
environment,
|
|
57
|
-
trustedOrigins: TRUSTED_ORIGINS[environment],
|
|
58
|
-
allowedCustomOrigins,
|
|
59
|
-
eventType: (_a = event.data) === null || _a === void 0 ? void 0 : _a.type,
|
|
60
|
-
});
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Validate SafePassage message format and content
|
|
65
|
-
*/
|
|
66
|
-
export function validateSafePassageMessage(event, expectedSessionId) {
|
|
67
|
-
const { data } = event;
|
|
68
|
-
// Check message format
|
|
69
|
-
if (!data || typeof data !== 'object') {
|
|
70
|
-
return { isValid: false, error: 'Invalid message format' };
|
|
71
|
-
}
|
|
72
|
-
// Check message type
|
|
73
|
-
if (data.type !== 'safepassage:verification:complete') {
|
|
74
|
-
return { isValid: false, error: 'Invalid message type' };
|
|
75
|
-
}
|
|
76
|
-
// Check session ID
|
|
77
|
-
if (!data.sessionId || data.sessionId !== expectedSessionId) {
|
|
78
|
-
return { isValid: false, error: 'Session ID mismatch' };
|
|
79
|
-
}
|
|
80
|
-
// Check status field
|
|
81
|
-
if (!data.status || !['verified', 'failed'].includes(data.status)) {
|
|
82
|
-
return { isValid: false, error: 'Invalid status value' };
|
|
83
|
-
}
|
|
84
|
-
return { isValid: true };
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Enforce HTTPS in production environment
|
|
88
|
-
*/
|
|
89
|
-
export function enforceHTTPS(environment) {
|
|
90
|
-
if (environment === 'production' && window.location.protocol !== 'https:') {
|
|
91
|
-
console.warn('SafePassage Warning: HTTPS recommended for production environment', {
|
|
92
|
-
current: window.location.href,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Validate URL security for return/cancel URLs
|
|
98
|
-
*/
|
|
99
|
-
export function validateReturnUrl(url, environment) {
|
|
100
|
-
try {
|
|
101
|
-
const parsed = new URL(url);
|
|
102
|
-
// Production and staging should use HTTPS, except for localhost (for development)
|
|
103
|
-
if (parsed.protocol !== 'https:') {
|
|
104
|
-
// Allow localhost for development use cases
|
|
105
|
-
const isLocalhost = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
|
|
106
|
-
if (!isLocalhost) {
|
|
107
|
-
return {
|
|
108
|
-
isValid: false,
|
|
109
|
-
error: `HTTPS required for return URLs in ${environment}`,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
// Block suspicious URLs
|
|
114
|
-
const suspiciousPatterns = [
|
|
115
|
-
/data:/i,
|
|
116
|
-
/javascript:/i,
|
|
117
|
-
/vbscript:/i,
|
|
118
|
-
/file:/i,
|
|
119
|
-
/ftp:/i,
|
|
120
|
-
];
|
|
121
|
-
for (const pattern of suspiciousPatterns) {
|
|
122
|
-
if (pattern.test(url)) {
|
|
123
|
-
return { isValid: false, error: 'Blocked suspicious URL scheme' };
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return { isValid: true };
|
|
127
|
-
}
|
|
128
|
-
catch (_a) {
|
|
129
|
-
return { isValid: false, error: 'Invalid URL format' };
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Generate secure session ID with entropy validation
|
|
134
|
-
*/
|
|
135
|
-
export function generateSecureSessionId() {
|
|
136
|
-
// Use crypto.randomUUID if available (modern browsers)
|
|
137
|
-
if (crypto.randomUUID) {
|
|
138
|
-
return crypto.randomUUID();
|
|
139
|
-
}
|
|
140
|
-
// Fallback to secure random generation
|
|
141
|
-
const array = new Uint8Array(16);
|
|
142
|
-
crypto.getRandomValues(array);
|
|
143
|
-
// Convert to UUID v4 format
|
|
144
|
-
const hex = Array.from(array)
|
|
145
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
146
|
-
.join('');
|
|
147
|
-
return [
|
|
148
|
-
hex.slice(0, 8),
|
|
149
|
-
hex.slice(8, 12),
|
|
150
|
-
'4' + hex.slice(13, 16), // Version 4
|
|
151
|
-
((parseInt(hex.slice(16, 17), 16) & 0x3) | 0x8).toString(16) +
|
|
152
|
-
hex.slice(17, 20), // Variant
|
|
153
|
-
hex.slice(20, 32),
|
|
154
|
-
].join('-');
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Rate limiting for verification attempts
|
|
158
|
-
*/
|
|
159
|
-
class VerificationRateLimit {
|
|
160
|
-
constructor() {
|
|
161
|
-
this.attempts = new Map();
|
|
162
|
-
this.maxAttempts = 5;
|
|
163
|
-
this.timeWindow = 60000; // 1 minute
|
|
164
|
-
}
|
|
165
|
-
isAllowed(identifier) {
|
|
166
|
-
const now = Date.now();
|
|
167
|
-
const attempts = this.attempts.get(identifier) || [];
|
|
168
|
-
// Filter out old attempts
|
|
169
|
-
const recentAttempts = attempts.filter((time) => now - time < this.timeWindow);
|
|
170
|
-
if (recentAttempts.length >= this.maxAttempts) {
|
|
171
|
-
console.warn(`SafePassage Security: Rate limit exceeded for ${identifier}`);
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
// Add current attempt
|
|
175
|
-
recentAttempts.push(now);
|
|
176
|
-
this.attempts.set(identifier, recentAttempts);
|
|
177
|
-
return true;
|
|
178
|
-
}
|
|
179
|
-
reset(identifier) {
|
|
180
|
-
this.attempts.delete(identifier);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
export const verificationRateLimit = new VerificationRateLimit();
|
|
184
|
-
/**
|
|
185
|
-
* Security event logging for monitoring
|
|
186
|
-
*/
|
|
187
|
-
export function logSecurityEvent(event, details) {
|
|
188
|
-
console.warn(`SafePassage Security Event: ${event}`, Object.assign({ timestamp: new Date().toISOString(), userAgent: navigator.userAgent, url: window.location.href }, details));
|
|
189
|
-
// In production, this could send events to a security monitoring service
|
|
190
|
-
}
|
package/dist/utils/validation.js
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validation utilities for SafePassage SDK
|
|
3
|
-
*/
|
|
4
|
-
import { validateReturnUrl } from './security';
|
|
5
|
-
export const MINIMUM_AGE = 25;
|
|
6
|
-
export const MAXIMUM_AGE = 150;
|
|
7
|
-
export const MAX_URL_LENGTH = 2048;
|
|
8
|
-
export const MAX_API_KEY_LENGTH = 128;
|
|
9
|
-
export const STATE_EXPIRY_MS = 600000; // 10 minutes
|
|
10
|
-
// SafePassage supports both public (pk_) and private (sk_) API keys
|
|
11
|
-
const API_KEY_PATTERN = /^(pk_|sk_)[a-zA-Z0-9_]+$/;
|
|
12
|
-
const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
13
|
-
export function validateConfig(config) {
|
|
14
|
-
if (!config.apiKey) {
|
|
15
|
-
throw new Error('apiKey is required');
|
|
16
|
-
}
|
|
17
|
-
if (config.apiKey.length > MAX_API_KEY_LENGTH) {
|
|
18
|
-
throw new Error(`apiKey exceeds maximum length of ${MAX_API_KEY_LENGTH} characters`);
|
|
19
|
-
}
|
|
20
|
-
if (!API_KEY_PATTERN.test(config.apiKey)) {
|
|
21
|
-
throw new Error('Invalid apiKey format. Expected pk_xxx (public) or sk_xxx (private)');
|
|
22
|
-
}
|
|
23
|
-
// Prevent Secret Keys from being used in browser code
|
|
24
|
-
if (typeof window !== 'undefined' && config.apiKey.startsWith('sk_')) {
|
|
25
|
-
throw new Error('Secret keys (sk_) should never be used in browser code for security reasons. ' +
|
|
26
|
-
'Secret keys expose your account to unauthorized access if used client-side. ' +
|
|
27
|
-
'Please use your public key (pk_) instead. ' +
|
|
28
|
-
'If you need to use features that require a secret key (like custom challenge age), ' +
|
|
29
|
-
'create the session server-side and pass the sessionId to startVerificationWithSession(). ' +
|
|
30
|
-
'See: https://docs.safepassageapp.com/server-side-sessions');
|
|
31
|
-
}
|
|
32
|
-
if (!config.returnUrl) {
|
|
33
|
-
throw new Error('returnUrl is required');
|
|
34
|
-
}
|
|
35
|
-
if (config.returnUrl.length > MAX_URL_LENGTH) {
|
|
36
|
-
throw new Error(`returnUrl exceeds maximum length of ${MAX_URL_LENGTH} characters`);
|
|
37
|
-
}
|
|
38
|
-
// Enhanced URL validation with security checks
|
|
39
|
-
const environment = detectEnvironment();
|
|
40
|
-
const returnUrlValidation = validateReturnUrl(config.returnUrl, environment);
|
|
41
|
-
if (!returnUrlValidation.isValid) {
|
|
42
|
-
throw new Error(`returnUrl validation failed: ${returnUrlValidation.error}`);
|
|
43
|
-
}
|
|
44
|
-
// cancelUrl is deprecated but still validate if provided for backwards compatibility
|
|
45
|
-
if (config.cancelUrl) {
|
|
46
|
-
if (config.cancelUrl.length > MAX_URL_LENGTH) {
|
|
47
|
-
throw new Error(`cancelUrl exceeds maximum length of ${MAX_URL_LENGTH} characters`);
|
|
48
|
-
}
|
|
49
|
-
const cancelUrlValidation = validateReturnUrl(config.cancelUrl, environment);
|
|
50
|
-
if (!cancelUrlValidation.isValid) {
|
|
51
|
-
throw new Error(`cancelUrl validation failed: ${cancelUrlValidation.error}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
if (config.defaultChallengeAge !== undefined) {
|
|
55
|
-
if (config.defaultChallengeAge < MINIMUM_AGE) {
|
|
56
|
-
throw new Error(`defaultChallengeAge must be at least ${MINIMUM_AGE}`);
|
|
57
|
-
}
|
|
58
|
-
if (config.defaultChallengeAge > MAXIMUM_AGE) {
|
|
59
|
-
throw new Error(`defaultChallengeAge cannot exceed ${MAXIMUM_AGE}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (config.defaultVerificationMode &&
|
|
63
|
-
!['L1', 'L2'].includes(config.defaultVerificationMode)) {
|
|
64
|
-
throw new Error('defaultVerificationMode must be L1 or L2');
|
|
65
|
-
}
|
|
66
|
-
if (config.mode && !['redirect', 'new-tab'].includes(config.mode)) {
|
|
67
|
-
throw new Error('mode must be redirect or new-tab');
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Detect environment based on current URL - defaults to production unless staging detected
|
|
72
|
-
*/
|
|
73
|
-
function detectEnvironment() {
|
|
74
|
-
// If no window (server environment), default to production for safety
|
|
75
|
-
if (typeof window === 'undefined') {
|
|
76
|
-
return 'production';
|
|
77
|
-
}
|
|
78
|
-
const hostname = window.location.hostname;
|
|
79
|
-
if (hostname.includes('staging') || hostname.includes('stage')) {
|
|
80
|
-
return 'staging';
|
|
81
|
-
}
|
|
82
|
-
return 'production';
|
|
83
|
-
}
|
|
84
|
-
export function validateSessionId(sessionId) {
|
|
85
|
-
if (!sessionId) {
|
|
86
|
-
throw new Error('sessionId is required');
|
|
87
|
-
}
|
|
88
|
-
if (!UUID_V4_PATTERN.test(sessionId)) {
|
|
89
|
-
throw new Error('sessionId must be a valid UUID v4');
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
export function validateChallengeAge(age) {
|
|
93
|
-
if (age !== undefined) {
|
|
94
|
-
if (age < MINIMUM_AGE) {
|
|
95
|
-
throw new Error(`challengeAge must be at least ${MINIMUM_AGE}`);
|
|
96
|
-
}
|
|
97
|
-
if (age > MAXIMUM_AGE) {
|
|
98
|
-
throw new Error(`challengeAge cannot exceed ${MAXIMUM_AGE}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
// URL validation now handled by security.ts validateReturnUrl function
|
|
103
|
-
/**
|
|
104
|
-
* Generate signed state parameter with HMAC protection
|
|
105
|
-
* Uses client-side HMAC for tamper resistance and server-side verification
|
|
106
|
-
*/
|
|
107
|
-
export async function generateState(payload, environment) {
|
|
108
|
-
// Import crypto utilities
|
|
109
|
-
const { createSignedState } = await import('./crypto');
|
|
110
|
-
return createSignedState(payload, environment);
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Parse and validate signed state parameter
|
|
114
|
-
*/
|
|
115
|
-
export async function parseState(state, environment) {
|
|
116
|
-
try {
|
|
117
|
-
// Import crypto utilities
|
|
118
|
-
const { parseSignedState } = await import('./crypto');
|
|
119
|
-
// Try parsing as signed state first (new format)
|
|
120
|
-
const signedPayload = await parseSignedState(state, environment);
|
|
121
|
-
if (signedPayload) {
|
|
122
|
-
// Type guard to ensure signedPayload has required properties
|
|
123
|
-
const payload = signedPayload;
|
|
124
|
-
// Validate payload structure (cancelUrl is deprecated and no longer required)
|
|
125
|
-
if (!payload.merchantId ||
|
|
126
|
-
!payload.sessionId ||
|
|
127
|
-
!payload.returnUrl) {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
// Cast to unknown first to satisfy TypeScript's type checking
|
|
131
|
-
return payload;
|
|
132
|
-
}
|
|
133
|
-
// Fallback to legacy base64 format for backwards compatibility
|
|
134
|
-
console.warn('SafePassage: Falling back to legacy state format - update your SDK');
|
|
135
|
-
const json = atob(state);
|
|
136
|
-
const payload = JSON.parse(json);
|
|
137
|
-
// Validate payload structure (cancelUrl is deprecated and no longer required)
|
|
138
|
-
if (!payload.merchantId ||
|
|
139
|
-
!payload.sessionId ||
|
|
140
|
-
!payload.returnUrl) {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
// Check timestamp expiration
|
|
144
|
-
const age = Date.now() - payload.timestamp;
|
|
145
|
-
if (age > STATE_EXPIRY_MS) {
|
|
146
|
-
console.warn('SafePassage: State parameter expired', {
|
|
147
|
-
age,
|
|
148
|
-
maxAge: STATE_EXPIRY_MS,
|
|
149
|
-
});
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
return payload;
|
|
153
|
-
}
|
|
154
|
-
catch (error) {
|
|
155
|
-
console.warn('SafePassage: Failed to parse state parameter', error);
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
}
|