@safepassage/sdk 3.0.3 → 3.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -4
- package/dist/core/SafePassageSDK.d.ts +68 -0
- package/dist/core/SafePassageSDK.js +392 -0
- package/dist/index.js +34 -7
- package/dist/safepassage.min.js +3 -3
- package/dist/tests/SafePassageSDK.test.d.ts +4 -0
- package/dist/tests/SafePassageSDK.test.js +130 -0
- package/dist/types/index.d.ts +129 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/crypto.d.ts +31 -0
- package/dist/utils/crypto.js +128 -0
- package/dist/utils/environment.d.ts +13 -0
- package/dist/utils/environment.js +88 -0
- package/dist/utils/polyfills.d.ts +12 -0
- package/dist/utils/polyfills.js +66 -0
- package/dist/utils/security.d.ts +50 -0
- package/dist/utils/security.js +208 -0
- package/dist/utils/validation.d.ts +21 -0
- package/dist/utils/validation.js +147 -0
- package/package.json +1 -1
- package/dist/components/SafePassageVerification.d.ts +0 -4
- package/dist/components/SafePassageVerification.js +0 -196
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for SafePassage SDK
|
|
3
|
+
*/
|
|
4
|
+
import { SafePassage } from '../core/SafePassageSDK';
|
|
5
|
+
describe('SafePassage SDK', () => {
|
|
6
|
+
let mockLocation;
|
|
7
|
+
let mockWindow;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// Mock window.location
|
|
10
|
+
mockLocation = {
|
|
11
|
+
href: 'https://merchant.com',
|
|
12
|
+
hostname: 'merchant.com',
|
|
13
|
+
origin: 'https://merchant.com'
|
|
14
|
+
};
|
|
15
|
+
// Mock window
|
|
16
|
+
mockWindow = {
|
|
17
|
+
location: mockLocation,
|
|
18
|
+
open: jest.fn(),
|
|
19
|
+
addEventListener: jest.fn(),
|
|
20
|
+
removeEventListener: jest.fn()
|
|
21
|
+
};
|
|
22
|
+
// Replace global window
|
|
23
|
+
global.window = mockWindow;
|
|
24
|
+
});
|
|
25
|
+
describe('constructor', () => {
|
|
26
|
+
it('should initialize with valid config', () => {
|
|
27
|
+
const config = {
|
|
28
|
+
apiKey: 'sk_test_123',
|
|
29
|
+
returnUrl: 'https://merchant.com/verified',
|
|
30
|
+
cancelUrl: 'https://merchant.com/cancelled'
|
|
31
|
+
};
|
|
32
|
+
const sp = new SafePassage(config);
|
|
33
|
+
expect(sp).toBeDefined();
|
|
34
|
+
});
|
|
35
|
+
it('should throw error for missing apiKey', () => {
|
|
36
|
+
const config = {
|
|
37
|
+
returnUrl: 'https://merchant.com/verified',
|
|
38
|
+
cancelUrl: 'https://merchant.com/cancelled'
|
|
39
|
+
};
|
|
40
|
+
expect(() => new SafePassage(config)).toThrow('apiKey is required');
|
|
41
|
+
});
|
|
42
|
+
it('should throw error for invalid apiKey format', () => {
|
|
43
|
+
const config = {
|
|
44
|
+
apiKey: 'invalid_key',
|
|
45
|
+
returnUrl: 'https://merchant.com/verified',
|
|
46
|
+
cancelUrl: 'https://merchant.com/cancelled'
|
|
47
|
+
};
|
|
48
|
+
expect(() => new SafePassage(config)).toThrow('Invalid apiKey format');
|
|
49
|
+
});
|
|
50
|
+
it('should throw error for missing returnUrl', () => {
|
|
51
|
+
const config = {
|
|
52
|
+
apiKey: 'sk_test_123',
|
|
53
|
+
cancelUrl: 'https://merchant.com/cancelled'
|
|
54
|
+
};
|
|
55
|
+
expect(() => new SafePassage(config)).toThrow('returnUrl is required');
|
|
56
|
+
});
|
|
57
|
+
it('should auto-detect development environment for localhost', () => {
|
|
58
|
+
mockLocation.hostname = 'localhost';
|
|
59
|
+
const config = {
|
|
60
|
+
apiKey: 'sk_test_123',
|
|
61
|
+
returnUrl: 'http://localhost:3000/verified',
|
|
62
|
+
cancelUrl: 'http://localhost:3000/cancelled'
|
|
63
|
+
};
|
|
64
|
+
const sp = new SafePassage(config);
|
|
65
|
+
expect(sp).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
it('should enforce minimum age of 25', () => {
|
|
68
|
+
const config = {
|
|
69
|
+
apiKey: 'sk_test_123',
|
|
70
|
+
returnUrl: 'https://merchant.com/verified',
|
|
71
|
+
cancelUrl: 'https://merchant.com/cancelled',
|
|
72
|
+
defaultChallengeAge: 21
|
|
73
|
+
};
|
|
74
|
+
expect(() => new SafePassage(config)).toThrow('defaultChallengeAge must be at least 25');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('verify', () => {
|
|
78
|
+
let sp;
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
const config = {
|
|
81
|
+
apiKey: 'sk_test_123',
|
|
82
|
+
returnUrl: 'https://merchant.com/verified',
|
|
83
|
+
cancelUrl: 'https://merchant.com/cancelled'
|
|
84
|
+
};
|
|
85
|
+
sp = new SafePassage(config);
|
|
86
|
+
});
|
|
87
|
+
it('should throw error for missing sessionId', () => {
|
|
88
|
+
expect(() => sp.verify({})).toThrow('sessionId is required');
|
|
89
|
+
});
|
|
90
|
+
it('should redirect in same-tab mode', () => {
|
|
91
|
+
const sessionId = '550e8400-e29b-41d4-a716-446655440000';
|
|
92
|
+
sp.verify({ sessionId });
|
|
93
|
+
expect(mockWindow.location.href).toContain('verify.safepassageapp.com');
|
|
94
|
+
expect(mockWindow.location.href).toContain(`sessionId=${sessionId}`);
|
|
95
|
+
});
|
|
96
|
+
it('should open new tab in new-tab mode', () => {
|
|
97
|
+
const config = {
|
|
98
|
+
apiKey: 'sk_test_123',
|
|
99
|
+
returnUrl: 'https://merchant.com/verified',
|
|
100
|
+
cancelUrl: 'https://merchant.com/cancelled',
|
|
101
|
+
mode: 'new-tab'
|
|
102
|
+
};
|
|
103
|
+
const sp = new SafePassage(config);
|
|
104
|
+
const sessionId = '550e8400-e29b-41d4-a716-446655440000';
|
|
105
|
+
sp.verify({ sessionId });
|
|
106
|
+
expect(mockWindow.open).toHaveBeenCalledWith(expect.stringContaining('verify.safepassageapp.com'), 'safepassage-verify', 'width=600,height=700');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe('URL building', () => {
|
|
110
|
+
it('should build correct URL with all parameters', () => {
|
|
111
|
+
const config = {
|
|
112
|
+
apiKey: 'sk_test_123',
|
|
113
|
+
returnUrl: 'https://merchant.com/verified',
|
|
114
|
+
cancelUrl: 'https://merchant.com/cancelled',
|
|
115
|
+
environment: 'staging'
|
|
116
|
+
};
|
|
117
|
+
const sp = new SafePassage(config);
|
|
118
|
+
// Access private method for testing
|
|
119
|
+
const buildUrl = sp.buildVerificationUrl.bind(sp);
|
|
120
|
+
const url = buildUrl({
|
|
121
|
+
sessionId: '550e8400-e29b-41d4-a716-446655440000',
|
|
122
|
+
challengeAge: 30,
|
|
123
|
+
verificationMode: 'L2'
|
|
124
|
+
});
|
|
125
|
+
expect(url).toContain('verify-staging.safepassageapp.com');
|
|
126
|
+
expect(url).toContain('sessionId=550e8400-e29b-41d4-a716-446655440000');
|
|
127
|
+
expect(url).toContain('state=');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SafePassage SDK Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
export interface SafePassageConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Public API key (starts with pk_live_ or pk_test_)
|
|
7
|
+
*/
|
|
8
|
+
apiKey: string;
|
|
9
|
+
/**
|
|
10
|
+
* URL to redirect to after successful verification
|
|
11
|
+
* Must be pre-registered in dashboard
|
|
12
|
+
*/
|
|
13
|
+
returnUrl: string;
|
|
14
|
+
/**
|
|
15
|
+
* URL to redirect to if user cancels verification
|
|
16
|
+
* Must be pre-registered in dashboard
|
|
17
|
+
*/
|
|
18
|
+
cancelUrl: string;
|
|
19
|
+
/**
|
|
20
|
+
* Environment to use
|
|
21
|
+
* @default Auto-detected based on hostname
|
|
22
|
+
*/
|
|
23
|
+
environment?: 'production' | 'staging' | 'development';
|
|
24
|
+
/**
|
|
25
|
+
* Verification mode
|
|
26
|
+
* @default 'redirect'
|
|
27
|
+
*/
|
|
28
|
+
mode?: 'redirect' | 'new-tab';
|
|
29
|
+
/**
|
|
30
|
+
* Default challenge age (minimum 25)
|
|
31
|
+
* Can be overridden per verification
|
|
32
|
+
*/
|
|
33
|
+
defaultChallengeAge?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Default verification mode
|
|
36
|
+
* Can be overridden per verification
|
|
37
|
+
*/
|
|
38
|
+
defaultVerificationMode?: 'L1' | 'L2';
|
|
39
|
+
/**
|
|
40
|
+
* Callback when verification completes (new-tab mode only)
|
|
41
|
+
*/
|
|
42
|
+
onComplete?: (result: VerificationResult) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Callback when user cancels (new-tab mode only)
|
|
45
|
+
*/
|
|
46
|
+
onCancel?: () => void;
|
|
47
|
+
/**
|
|
48
|
+
* Callback for errors
|
|
49
|
+
*/
|
|
50
|
+
onError?: (error: Error) => void;
|
|
51
|
+
}
|
|
52
|
+
export interface VerificationOptions {
|
|
53
|
+
/**
|
|
54
|
+
* Merchant-generated UUID v4 for this verification session
|
|
55
|
+
* Required for private keys (sk_), optional for public keys (pk_)
|
|
56
|
+
* For public keys: SDK will generate session internally
|
|
57
|
+
*/
|
|
58
|
+
sessionId?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Minimum age to verify (minimum 25)
|
|
61
|
+
* @default Uses merchant dashboard configuration
|
|
62
|
+
*/
|
|
63
|
+
challengeAge?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Verification mode
|
|
66
|
+
* L1: Age estimation allowed if user appears older
|
|
67
|
+
* L2: Full ID verification required
|
|
68
|
+
* @default Uses merchant dashboard configuration
|
|
69
|
+
*/
|
|
70
|
+
verificationMode?: 'L1' | 'L2';
|
|
71
|
+
/**
|
|
72
|
+
* External user identifier from merchant system
|
|
73
|
+
* Optional parameter that will be returned with verification results
|
|
74
|
+
* Useful for correlating SafePassage sessions with merchant user records
|
|
75
|
+
*/
|
|
76
|
+
externalUserId?: string;
|
|
77
|
+
}
|
|
78
|
+
export interface VerificationResult {
|
|
79
|
+
/**
|
|
80
|
+
* The session ID that was verified
|
|
81
|
+
*/
|
|
82
|
+
sessionId: string;
|
|
83
|
+
/**
|
|
84
|
+
* Binary result: 'verified' or 'failed'
|
|
85
|
+
* Full details available via server-side API
|
|
86
|
+
*/
|
|
87
|
+
status: 'verified' | 'failed' | 'cancelled';
|
|
88
|
+
/**
|
|
89
|
+
* External user identifier if provided during verification
|
|
90
|
+
*/
|
|
91
|
+
externalUserId?: string;
|
|
92
|
+
}
|
|
93
|
+
export interface StatePayload {
|
|
94
|
+
merchantId: string;
|
|
95
|
+
sessionId: string;
|
|
96
|
+
returnUrl: string;
|
|
97
|
+
cancelUrl: string;
|
|
98
|
+
challengeAge?: number;
|
|
99
|
+
verificationMode?: 'L1' | 'L2';
|
|
100
|
+
hasOverrides?: boolean;
|
|
101
|
+
externalUserId?: string;
|
|
102
|
+
timestamp: number;
|
|
103
|
+
}
|
|
104
|
+
export interface SessionValidationResponse {
|
|
105
|
+
sessionId: string;
|
|
106
|
+
merchantId: string;
|
|
107
|
+
status: 'verified' | 'failed';
|
|
108
|
+
verified: boolean;
|
|
109
|
+
estimatedAge?: number;
|
|
110
|
+
challengeAge: number;
|
|
111
|
+
verificationMode: 'L1' | 'L2';
|
|
112
|
+
verificationMethod?: 'facial' | 'document' | 'combined';
|
|
113
|
+
timestamp: string;
|
|
114
|
+
expiresAt: string;
|
|
115
|
+
}
|
|
116
|
+
export interface SessionCreationResponse {
|
|
117
|
+
sessionToken: string;
|
|
118
|
+
verifyUrl: string;
|
|
119
|
+
expiresAt: string;
|
|
120
|
+
}
|
|
121
|
+
export interface CreateSessionRequest {
|
|
122
|
+
sessionId: string;
|
|
123
|
+
returnUrl: string;
|
|
124
|
+
cancelUrl?: string;
|
|
125
|
+
challengeAge?: number;
|
|
126
|
+
verificationMode?: 'L1' | 'L2';
|
|
127
|
+
merchantName?: string;
|
|
128
|
+
externalUserId?: string;
|
|
129
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crypto utilities for SafePassage SDK - Browser compatible
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Generate HMAC-SHA256 signature using Web Crypto API
|
|
6
|
+
* Note: This is client-side HMAC, the secret is known to the client.
|
|
7
|
+
* For true security, verification should still happen server-side.
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateHMAC(data: string, secret: string): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Verify HMAC-SHA256 signature using Web Crypto API
|
|
12
|
+
*/
|
|
13
|
+
export declare function verifyHMAC(data: string, signature: string, secret: string): Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* Generate a secure random token for client-side use
|
|
16
|
+
*/
|
|
17
|
+
export declare function generateSecureToken(length?: number): string;
|
|
18
|
+
/**
|
|
19
|
+
* Environment-specific secret derivation
|
|
20
|
+
* In a real implementation, this would derive from environment variables
|
|
21
|
+
* For client-side use, this provides basic tamper resistance
|
|
22
|
+
*/
|
|
23
|
+
export declare function getSigningSecret(environment: 'production' | 'staging' | 'development'): string;
|
|
24
|
+
/**
|
|
25
|
+
* Create signed state parameter with timestamp and integrity protection
|
|
26
|
+
*/
|
|
27
|
+
export declare function createSignedState(payload: any, environment: 'production' | 'staging' | 'development'): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Verify and parse signed state parameter
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseSignedState(signedState: string, environment: 'production' | 'staging' | 'development', maxAge?: number): Promise<any | null>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crypto utilities for SafePassage SDK - Browser compatible
|
|
3
|
+
*/
|
|
4
|
+
import { STATE_EXPIRY_MS } from './validation';
|
|
5
|
+
/**
|
|
6
|
+
* Generate HMAC-SHA256 signature using Web Crypto API
|
|
7
|
+
* Note: This is client-side HMAC, the secret is known to the client.
|
|
8
|
+
* For true security, verification should still happen server-side.
|
|
9
|
+
*/
|
|
10
|
+
export async function generateHMAC(data, secret) {
|
|
11
|
+
// Encode the secret and data as Uint8Array
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
const keyData = encoder.encode(secret);
|
|
14
|
+
const dataBuffer = encoder.encode(data);
|
|
15
|
+
// Import the secret as a key
|
|
16
|
+
const key = await crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
17
|
+
// Generate the signature
|
|
18
|
+
const signature = await crypto.subtle.sign('HMAC', key, dataBuffer);
|
|
19
|
+
// Convert to hex string
|
|
20
|
+
return Array.from(new Uint8Array(signature))
|
|
21
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
22
|
+
.join('');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Verify HMAC-SHA256 signature using Web Crypto API
|
|
26
|
+
*/
|
|
27
|
+
export async function verifyHMAC(data, signature, secret) {
|
|
28
|
+
try {
|
|
29
|
+
const expectedSignature = await generateHMAC(data, secret);
|
|
30
|
+
return constantTimeCompare(signature, expectedSignature);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Constant-time string comparison to prevent timing attacks
|
|
38
|
+
*/
|
|
39
|
+
function constantTimeCompare(a, b) {
|
|
40
|
+
if (a.length !== b.length) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
let result = 0;
|
|
44
|
+
for (let i = 0; i < a.length; i++) {
|
|
45
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
46
|
+
}
|
|
47
|
+
return result === 0;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Generate a secure random token for client-side use
|
|
51
|
+
*/
|
|
52
|
+
export function generateSecureToken(length = 32) {
|
|
53
|
+
const array = new Uint8Array(length);
|
|
54
|
+
crypto.getRandomValues(array);
|
|
55
|
+
return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Environment-specific secret derivation
|
|
59
|
+
* In a real implementation, this would derive from environment variables
|
|
60
|
+
* For client-side use, this provides basic tamper resistance
|
|
61
|
+
*/
|
|
62
|
+
export function getSigningSecret(environment) {
|
|
63
|
+
// WARNING: Client-side secrets are NOT secure against determined attackers
|
|
64
|
+
// This is defense-in-depth only. Real security comes from server-side validation.
|
|
65
|
+
const baseSecrets = {
|
|
66
|
+
production: 'safepassage-prod-hmac-2025',
|
|
67
|
+
staging: 'safepassage-stage-hmac-2025',
|
|
68
|
+
development: 'safepassage-dev-hmac-2025'
|
|
69
|
+
};
|
|
70
|
+
return baseSecrets[environment];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create signed state parameter with timestamp and integrity protection
|
|
74
|
+
*/
|
|
75
|
+
export async function createSignedState(payload, environment) {
|
|
76
|
+
// Add timestamp for freshness
|
|
77
|
+
const timestampedPayload = {
|
|
78
|
+
...payload,
|
|
79
|
+
timestamp: Date.now(),
|
|
80
|
+
nonce: generateSecureToken(16) // Add nonce to prevent replay attacks
|
|
81
|
+
};
|
|
82
|
+
const dataString = JSON.stringify(timestampedPayload);
|
|
83
|
+
const secret = getSigningSecret(environment);
|
|
84
|
+
const signature = await generateHMAC(dataString, secret);
|
|
85
|
+
// Combine data and signature
|
|
86
|
+
const signedPayload = {
|
|
87
|
+
data: timestampedPayload,
|
|
88
|
+
signature
|
|
89
|
+
};
|
|
90
|
+
return btoa(JSON.stringify(signedPayload));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Verify and parse signed state parameter
|
|
94
|
+
*/
|
|
95
|
+
export async function parseSignedState(signedState, environment, maxAge = STATE_EXPIRY_MS) {
|
|
96
|
+
try {
|
|
97
|
+
const json = atob(signedState);
|
|
98
|
+
const signedPayload = JSON.parse(json);
|
|
99
|
+
if (!signedPayload.data || !signedPayload.signature) {
|
|
100
|
+
console.warn('SafePassage: Invalid signed state format');
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const { data, signature } = signedPayload;
|
|
104
|
+
const dataString = JSON.stringify(data);
|
|
105
|
+
const secret = getSigningSecret(environment);
|
|
106
|
+
// Verify signature
|
|
107
|
+
const isValid = await verifyHMAC(dataString, signature, secret);
|
|
108
|
+
if (!isValid) {
|
|
109
|
+
console.warn('SafePassage: State signature verification failed');
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
// Check timestamp freshness
|
|
113
|
+
if (data.timestamp) {
|
|
114
|
+
const age = Date.now() - data.timestamp;
|
|
115
|
+
if (age > maxAge) {
|
|
116
|
+
console.warn('SafePassage: State parameter expired', { age, maxAge });
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Remove internal fields before returning
|
|
121
|
+
const { timestamp, nonce, ...payload } = data;
|
|
122
|
+
return payload;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
console.warn('SafePassage: Failed to parse signed state', error);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment utilities for SafePassage SDK with security enforcement
|
|
3
|
+
*/
|
|
4
|
+
export declare function getEnvironmentUrl(environment: 'production' | 'staging' | 'development'): string;
|
|
5
|
+
export declare function getApiUrl(environment: 'production' | 'staging' | 'development'): string;
|
|
6
|
+
/**
|
|
7
|
+
* Detect environment with security warnings
|
|
8
|
+
*/
|
|
9
|
+
export declare function detectEnvironment(): 'production' | 'staging' | 'development';
|
|
10
|
+
/**
|
|
11
|
+
* Validate environment configuration on startup
|
|
12
|
+
*/
|
|
13
|
+
export declare function validateEnvironmentSecurity(environment: 'production' | 'staging' | 'development'): void;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment utilities for SafePassage SDK with security enforcement
|
|
3
|
+
*/
|
|
4
|
+
const ENVIRONMENT_URLS = {
|
|
5
|
+
production: 'https://verify.safepassageapp.com',
|
|
6
|
+
staging: 'https://verify-staging.safepassageapp.com',
|
|
7
|
+
development: 'http://localhost:5173'
|
|
8
|
+
};
|
|
9
|
+
export function getEnvironmentUrl(environment) {
|
|
10
|
+
const url = ENVIRONMENT_URLS[environment];
|
|
11
|
+
// Security check: Ensure production/staging always use HTTPS
|
|
12
|
+
if ((environment === 'production' || environment === 'staging') && !url.startsWith('https://')) {
|
|
13
|
+
throw new Error(`HTTPS required for ${environment} environment`);
|
|
14
|
+
}
|
|
15
|
+
return url;
|
|
16
|
+
}
|
|
17
|
+
export function getApiUrl(environment) {
|
|
18
|
+
const apiUrls = {
|
|
19
|
+
production: 'https://api.safepassageapp.com',
|
|
20
|
+
staging: 'https://api-staging.safepassageapp.com',
|
|
21
|
+
development: 'http://localhost:3001'
|
|
22
|
+
};
|
|
23
|
+
const url = apiUrls[environment];
|
|
24
|
+
// Security check: Ensure production/staging always use HTTPS
|
|
25
|
+
if ((environment === 'production' || environment === 'staging') && !url.startsWith('https://')) {
|
|
26
|
+
throw new Error(`HTTPS required for API URLs in ${environment} environment`);
|
|
27
|
+
}
|
|
28
|
+
return url;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Detect environment with security warnings
|
|
32
|
+
*/
|
|
33
|
+
export function detectEnvironment() {
|
|
34
|
+
const hostname = window.location.hostname;
|
|
35
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.includes('.local')) {
|
|
36
|
+
// Warn if using HTTP in development with non-localhost domains
|
|
37
|
+
if (window.location.protocol === 'http:' && !hostname.match(/^(localhost|127\.0\.0\.1)$/)) {
|
|
38
|
+
console.warn('SafePassage Security Warning: Using HTTP with non-localhost domain in development');
|
|
39
|
+
}
|
|
40
|
+
return 'development';
|
|
41
|
+
}
|
|
42
|
+
if (hostname.includes('staging') || hostname.includes('stage')) {
|
|
43
|
+
// Enforce HTTPS in staging
|
|
44
|
+
if (window.location.protocol !== 'https:') {
|
|
45
|
+
console.error('SafePassage Security Error: HTTPS required in staging environment');
|
|
46
|
+
}
|
|
47
|
+
return 'staging';
|
|
48
|
+
}
|
|
49
|
+
// Production environment - strict HTTPS enforcement
|
|
50
|
+
if (window.location.protocol !== 'https:') {
|
|
51
|
+
console.error('SafePassage Security Error: HTTPS required in production environment');
|
|
52
|
+
}
|
|
53
|
+
return 'production';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Validate environment configuration on startup
|
|
57
|
+
*/
|
|
58
|
+
export function validateEnvironmentSecurity(environment) {
|
|
59
|
+
// Check current page protocol
|
|
60
|
+
const isSecure = window.location.protocol === 'https:';
|
|
61
|
+
const hostname = window.location.hostname;
|
|
62
|
+
switch (environment) {
|
|
63
|
+
case 'production':
|
|
64
|
+
if (!isSecure) {
|
|
65
|
+
throw new Error('SafePassage requires HTTPS in production environment');
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
case 'staging':
|
|
69
|
+
if (!isSecure) {
|
|
70
|
+
console.warn('SafePassage Warning: HTTPS strongly recommended in staging environment');
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
case 'development':
|
|
74
|
+
const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname.includes('.local');
|
|
75
|
+
if (!isSecure && !isLocalhost) {
|
|
76
|
+
console.warn('SafePassage Warning: HTTPS recommended for non-localhost development');
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
// Validate environment URLs
|
|
81
|
+
try {
|
|
82
|
+
getEnvironmentUrl(environment);
|
|
83
|
+
getApiUrl(environment);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw new Error(`Environment configuration validation failed: ${error}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polyfills for browser compatibility
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Polyfill for crypto.randomUUID (not supported in older browsers)
|
|
6
|
+
* Generates a UUID v4 using crypto.getRandomValues
|
|
7
|
+
*/
|
|
8
|
+
export declare function setupPolyfills(): void;
|
|
9
|
+
/**
|
|
10
|
+
* Check browser compatibility and warn about unsupported features
|
|
11
|
+
*/
|
|
12
|
+
export declare function checkBrowserCompatibility(): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polyfills for browser compatibility
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Polyfill for crypto.randomUUID (not supported in older browsers)
|
|
6
|
+
* Generates a UUID v4 using crypto.getRandomValues
|
|
7
|
+
*/
|
|
8
|
+
export function setupPolyfills() {
|
|
9
|
+
// Polyfill crypto.randomUUID if not available
|
|
10
|
+
if (!crypto.randomUUID) {
|
|
11
|
+
crypto.randomUUID = function () {
|
|
12
|
+
// Generate 16 random bytes
|
|
13
|
+
const array = new Uint8Array(16);
|
|
14
|
+
crypto.getRandomValues(array);
|
|
15
|
+
// Set version (4) and variant bits
|
|
16
|
+
array[6] = (array[6] & 0x0f) | 0x40; // Version 4
|
|
17
|
+
array[8] = (array[8] & 0x3f) | 0x80; // Variant 10
|
|
18
|
+
// Convert to hex string with dashes
|
|
19
|
+
const hex = Array.from(array).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
20
|
+
return [
|
|
21
|
+
hex.slice(0, 8),
|
|
22
|
+
hex.slice(8, 12),
|
|
23
|
+
hex.slice(12, 16),
|
|
24
|
+
hex.slice(16, 20),
|
|
25
|
+
hex.slice(20, 32)
|
|
26
|
+
].join('-');
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// Note: URLSearchParams is widely supported (IE 11 doesn't support it, but we're not targeting IE 11)
|
|
30
|
+
// If needed, a polyfill can be added here
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Check browser compatibility and warn about unsupported features
|
|
34
|
+
*/
|
|
35
|
+
export function checkBrowserCompatibility() {
|
|
36
|
+
const warnings = [];
|
|
37
|
+
// Check for required features
|
|
38
|
+
if (!window.crypto || !window.crypto.getRandomValues) {
|
|
39
|
+
throw new Error('SafePassage SDK requires Web Crypto API support');
|
|
40
|
+
}
|
|
41
|
+
if (!window.crypto.subtle) {
|
|
42
|
+
throw new Error('SafePassage SDK requires Web Crypto subtle API for HMAC operations');
|
|
43
|
+
}
|
|
44
|
+
// Check for features that we can polyfill
|
|
45
|
+
if (!crypto.randomUUID) {
|
|
46
|
+
warnings.push('crypto.randomUUID not supported, using polyfill');
|
|
47
|
+
}
|
|
48
|
+
if (!window.URLSearchParams) {
|
|
49
|
+
warnings.push('URLSearchParams not supported, consider adding a polyfill for IE 11 support');
|
|
50
|
+
}
|
|
51
|
+
// Check for modern JavaScript features
|
|
52
|
+
try {
|
|
53
|
+
// Test optional chaining
|
|
54
|
+
const test = { a: { b: 1 } };
|
|
55
|
+
const result = test?.a?.b;
|
|
56
|
+
if (result !== 1)
|
|
57
|
+
throw new Error();
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
warnings.push('Optional chaining (?.) not supported, ensure transpilation for older browsers');
|
|
61
|
+
}
|
|
62
|
+
// Log warnings if any
|
|
63
|
+
if (warnings.length > 0) {
|
|
64
|
+
console.warn('SafePassage SDK Browser Compatibility:', warnings.join('; '));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security utilities for SafePassage SDK
|
|
3
|
+
* Enhanced origin validation and security enforcement
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Validate if an origin is trusted for the given environment
|
|
7
|
+
*/
|
|
8
|
+
export declare function isOriginTrusted(origin: string, environment: 'production' | 'staging' | 'development'): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Enhanced origin validation with logging and strict allowlist
|
|
11
|
+
*/
|
|
12
|
+
export declare function validatePostMessageOrigin(event: MessageEvent, environment: 'production' | 'staging' | 'development', allowedCustomOrigins?: string[]): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Validate SafePassage message format and content
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateSafePassageMessage(event: MessageEvent, expectedSessionId: string): {
|
|
17
|
+
isValid: boolean;
|
|
18
|
+
error?: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Enforce HTTPS in production environment
|
|
22
|
+
*/
|
|
23
|
+
export declare function enforceHTTPS(environment: 'production' | 'staging' | 'development'): void;
|
|
24
|
+
/**
|
|
25
|
+
* Validate URL security for return/cancel URLs
|
|
26
|
+
*/
|
|
27
|
+
export declare function validateReturnUrl(url: string, environment: 'production' | 'staging' | 'development'): {
|
|
28
|
+
isValid: boolean;
|
|
29
|
+
error?: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Generate secure session ID with entropy validation
|
|
33
|
+
*/
|
|
34
|
+
export declare function generateSecureSessionId(): string;
|
|
35
|
+
/**
|
|
36
|
+
* Rate limiting for verification attempts
|
|
37
|
+
*/
|
|
38
|
+
declare class VerificationRateLimit {
|
|
39
|
+
private attempts;
|
|
40
|
+
private readonly maxAttempts;
|
|
41
|
+
private readonly timeWindow;
|
|
42
|
+
isAllowed(identifier: string): boolean;
|
|
43
|
+
reset(identifier: string): void;
|
|
44
|
+
}
|
|
45
|
+
export declare const verificationRateLimit: VerificationRateLimit;
|
|
46
|
+
/**
|
|
47
|
+
* Security event logging for monitoring
|
|
48
|
+
*/
|
|
49
|
+
export declare function logSecurityEvent(event: string, details: any): void;
|
|
50
|
+
export {};
|