@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.
@@ -8,14 +8,14 @@
8
8
  */
9
9
  const TRUSTED_ORIGINS = {
10
10
  production: [
11
- 'https://verify.safepassageapp.com',
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://verify-staging.safepassageapp.com',
17
- 'https://portal-staging.safepassageapp.com',
18
- 'https://api-staging.safepassageapp.com'
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}`) || origin === `https://${domain}` || origin === `http://${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 || !['verified', 'failed', 'cancelled'].includes(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 { isValid: false, error: 'HTTPS required for return URLs in production' };
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 { isValid: false, error: 'HTTPS required for return URLs in staging' };
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 (error) {
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).map(b => b.toString(16).padStart(2, '0')).join('');
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) + hex.slice(17, 20), // Variant
166
- hex.slice(20, 32)
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
  }
@@ -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 && !['L1', 'L2'].includes(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' || hostname === '127.0.0.1' || hostname.includes('.local')) {
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 (!signedPayload.merchantId || !signedPayload.sessionId ||
122
- !signedPayload.returnUrl || !signedPayload.cancelUrl) {
126
+ if (!payload.merchantId ||
127
+ !payload.sessionId ||
128
+ !payload.returnUrl ||
129
+ !payload.cancelUrl) {
123
130
  return null;
124
131
  }
125
- return signedPayload;
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 || !payload.sessionId || !payload.returnUrl || !payload.cancelUrl) {
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', { age, maxAge: STATE_EXPIRY_MS });
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.4",
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 --config test/jest.config.js",
18
- "test:browser": "jest --config test/jest.config.js test/browser-compat",
19
- "test:mobile": "jest --config test/jest.config.js test/mobile",
20
- "test:performance": "jest --config test/jest.config.js test/performance",
21
- "test:security": "jest --config test/jest.config.js test/security",
22
- "test:accessibility": "jest --config test/jest.config.js test/accessibility",
23
- "test:edge": "jest --config test/jest.config.js test/edge-cases",
24
- "test:integration": "jest --config test/jest.config.js test/integration",
25
- "test:all": "node test/run-tests.js",
26
- "test:coverage": "jest --config test/jest.config.js --coverage",
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
- "@types/jest": "^29.5.12",
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.1.2",
57
+ "ts-jest": "^29.4.0",
54
58
  "typescript": "~5.8.3"
55
59
  },
56
60
  "repository": {