@safepassage/sdk 3.4.8 → 3.4.10

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.
@@ -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
- }
@@ -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
- }