@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,208 @@
|
|
|
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://verify.safepassageapp.com',
|
|
12
|
+
'https://portal.safepassageapp.com',
|
|
13
|
+
'https://api.safepassageapp.com'
|
|
14
|
+
],
|
|
15
|
+
staging: [
|
|
16
|
+
'https://verify-staging.safepassageapp.com',
|
|
17
|
+
'https://portal-staging.safepassageapp.com',
|
|
18
|
+
'https://api-staging.safepassageapp.com'
|
|
19
|
+
],
|
|
20
|
+
development: [
|
|
21
|
+
'http://localhost:5173',
|
|
22
|
+
'http://localhost:3000',
|
|
23
|
+
'http://localhost:3001',
|
|
24
|
+
'http://localhost:3002',
|
|
25
|
+
'http://127.0.0.1:5173',
|
|
26
|
+
'http://127.0.0.1:3000',
|
|
27
|
+
'http://127.0.0.1:3001',
|
|
28
|
+
'http://127.0.0.1:3002'
|
|
29
|
+
]
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Validate if an origin is trusted for the given environment
|
|
33
|
+
*/
|
|
34
|
+
export function isOriginTrusted(origin, environment) {
|
|
35
|
+
const trustedOrigins = TRUSTED_ORIGINS[environment];
|
|
36
|
+
return trustedOrigins.includes(origin);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Enhanced origin validation with logging and strict allowlist
|
|
40
|
+
*/
|
|
41
|
+
export function validatePostMessageOrigin(event, environment, allowedCustomOrigins = []) {
|
|
42
|
+
const { origin } = event;
|
|
43
|
+
// Check against trusted SafePassage origins
|
|
44
|
+
if (isOriginTrusted(origin, environment)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
// Check against custom allowed origins (for merchant websites)
|
|
48
|
+
if (allowedCustomOrigins.length > 0) {
|
|
49
|
+
const isCustomOriginAllowed = allowedCustomOrigins.some(allowedOrigin => {
|
|
50
|
+
// Support wildcard subdomains (e.g., "*.example.com")
|
|
51
|
+
if (allowedOrigin.startsWith('*.')) {
|
|
52
|
+
const domain = allowedOrigin.slice(2);
|
|
53
|
+
return origin.endsWith(`.${domain}`) || origin === `https://${domain}` || origin === `http://${domain}`;
|
|
54
|
+
}
|
|
55
|
+
return origin === allowedOrigin;
|
|
56
|
+
});
|
|
57
|
+
if (isCustomOriginAllowed) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Log security violation for debugging
|
|
62
|
+
console.warn(`SafePassage Security: Blocked PostMessage from untrusted origin: ${origin}`, {
|
|
63
|
+
environment,
|
|
64
|
+
trustedOrigins: TRUSTED_ORIGINS[environment],
|
|
65
|
+
allowedCustomOrigins,
|
|
66
|
+
eventType: event.data?.type
|
|
67
|
+
});
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Validate SafePassage message format and content
|
|
72
|
+
*/
|
|
73
|
+
export function validateSafePassageMessage(event, expectedSessionId) {
|
|
74
|
+
const { data } = event;
|
|
75
|
+
// Check message format
|
|
76
|
+
if (!data || typeof data !== 'object') {
|
|
77
|
+
return { isValid: false, error: 'Invalid message format' };
|
|
78
|
+
}
|
|
79
|
+
// Check message type
|
|
80
|
+
if (data.type !== 'safepassage:verification:complete') {
|
|
81
|
+
return { isValid: false, error: 'Invalid message type' };
|
|
82
|
+
}
|
|
83
|
+
// Check session ID
|
|
84
|
+
if (!data.sessionId || data.sessionId !== expectedSessionId) {
|
|
85
|
+
return { isValid: false, error: 'Session ID mismatch' };
|
|
86
|
+
}
|
|
87
|
+
// Check status field
|
|
88
|
+
if (!data.status || !['verified', 'failed', 'cancelled'].includes(data.status)) {
|
|
89
|
+
return { isValid: false, error: 'Invalid status value' };
|
|
90
|
+
}
|
|
91
|
+
return { isValid: true };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Enforce HTTPS in production environment
|
|
95
|
+
*/
|
|
96
|
+
export function enforceHTTPS(environment) {
|
|
97
|
+
if (environment === 'production' && window.location.protocol !== 'https:') {
|
|
98
|
+
const httpsUrl = window.location.href.replace('http:', 'https:');
|
|
99
|
+
console.error('SafePassage Security: HTTPS required in production. Redirecting...', {
|
|
100
|
+
current: window.location.href,
|
|
101
|
+
redirect: httpsUrl
|
|
102
|
+
});
|
|
103
|
+
window.location.replace(httpsUrl);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Validate URL security for return/cancel URLs
|
|
108
|
+
*/
|
|
109
|
+
export function validateReturnUrl(url, environment) {
|
|
110
|
+
try {
|
|
111
|
+
const parsed = new URL(url);
|
|
112
|
+
// Production must use HTTPS
|
|
113
|
+
if (environment === 'production' && parsed.protocol !== 'https:') {
|
|
114
|
+
return { isValid: false, error: 'HTTPS required for return URLs in production' };
|
|
115
|
+
}
|
|
116
|
+
// Development allows HTTP localhost
|
|
117
|
+
if (environment === 'development') {
|
|
118
|
+
const isLocalhost = parsed.hostname === 'localhost' ||
|
|
119
|
+
parsed.hostname === '127.0.0.1' ||
|
|
120
|
+
parsed.hostname.endsWith('.local');
|
|
121
|
+
if (!isLocalhost && parsed.protocol !== 'https:') {
|
|
122
|
+
return { isValid: false, error: 'Non-localhost URLs must use HTTPS' };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Staging should use HTTPS
|
|
126
|
+
if (environment === 'staging' && parsed.protocol !== 'https:') {
|
|
127
|
+
return { isValid: false, error: 'HTTPS required for return URLs in staging' };
|
|
128
|
+
}
|
|
129
|
+
// Block suspicious URLs
|
|
130
|
+
const suspiciousPatterns = [
|
|
131
|
+
/data:/i,
|
|
132
|
+
/javascript:/i,
|
|
133
|
+
/vbscript:/i,
|
|
134
|
+
/file:/i,
|
|
135
|
+
/ftp:/i
|
|
136
|
+
];
|
|
137
|
+
for (const pattern of suspiciousPatterns) {
|
|
138
|
+
if (pattern.test(url)) {
|
|
139
|
+
return { isValid: false, error: 'Blocked suspicious URL scheme' };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { isValid: true };
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return { isValid: false, error: 'Invalid URL format' };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Generate secure session ID with entropy validation
|
|
150
|
+
*/
|
|
151
|
+
export function generateSecureSessionId() {
|
|
152
|
+
// Use crypto.randomUUID if available (modern browsers)
|
|
153
|
+
if (crypto.randomUUID) {
|
|
154
|
+
return crypto.randomUUID();
|
|
155
|
+
}
|
|
156
|
+
// Fallback to secure random generation
|
|
157
|
+
const array = new Uint8Array(16);
|
|
158
|
+
crypto.getRandomValues(array);
|
|
159
|
+
// Convert to UUID v4 format
|
|
160
|
+
const hex = Array.from(array).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
161
|
+
return [
|
|
162
|
+
hex.slice(0, 8),
|
|
163
|
+
hex.slice(8, 12),
|
|
164
|
+
'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)
|
|
167
|
+
].join('-');
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Rate limiting for verification attempts
|
|
171
|
+
*/
|
|
172
|
+
class VerificationRateLimit {
|
|
173
|
+
constructor() {
|
|
174
|
+
this.attempts = new Map();
|
|
175
|
+
this.maxAttempts = 5;
|
|
176
|
+
this.timeWindow = 60000; // 1 minute
|
|
177
|
+
}
|
|
178
|
+
isAllowed(identifier) {
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
const attempts = this.attempts.get(identifier) || [];
|
|
181
|
+
// Filter out old attempts
|
|
182
|
+
const recentAttempts = attempts.filter(time => now - time < this.timeWindow);
|
|
183
|
+
if (recentAttempts.length >= this.maxAttempts) {
|
|
184
|
+
console.warn(`SafePassage Security: Rate limit exceeded for ${identifier}`);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
// Add current attempt
|
|
188
|
+
recentAttempts.push(now);
|
|
189
|
+
this.attempts.set(identifier, recentAttempts);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
reset(identifier) {
|
|
193
|
+
this.attempts.delete(identifier);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
export const verificationRateLimit = new VerificationRateLimit();
|
|
197
|
+
/**
|
|
198
|
+
* Security event logging for monitoring
|
|
199
|
+
*/
|
|
200
|
+
export function logSecurityEvent(event, details) {
|
|
201
|
+
console.warn(`SafePassage Security Event: ${event}`, {
|
|
202
|
+
timestamp: new Date().toISOString(),
|
|
203
|
+
userAgent: navigator.userAgent,
|
|
204
|
+
url: window.location.href,
|
|
205
|
+
...details
|
|
206
|
+
});
|
|
207
|
+
// In production, this could send events to a security monitoring service
|
|
208
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities for SafePassage SDK
|
|
3
|
+
*/
|
|
4
|
+
import type { SafePassageConfig, StatePayload } from '../types';
|
|
5
|
+
export declare const MINIMUM_AGE = 25;
|
|
6
|
+
export declare const MAXIMUM_AGE = 150;
|
|
7
|
+
export declare const MAX_URL_LENGTH = 2048;
|
|
8
|
+
export declare const MAX_API_KEY_LENGTH = 128;
|
|
9
|
+
export declare const STATE_EXPIRY_MS = 600000;
|
|
10
|
+
export declare function validateConfig(config: SafePassageConfig): void;
|
|
11
|
+
export declare function validateSessionId(sessionId: string): void;
|
|
12
|
+
export declare function validateChallengeAge(age?: number): void;
|
|
13
|
+
/**
|
|
14
|
+
* Generate signed state parameter with HMAC protection
|
|
15
|
+
* Uses client-side HMAC for tamper resistance and server-side verification
|
|
16
|
+
*/
|
|
17
|
+
export declare function generateState(payload: StatePayload, environment: 'production' | 'staging' | 'development'): Promise<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Parse and validate signed state parameter
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseState(state: string, environment: 'production' | 'staging' | 'development'): Promise<StatePayload | null>;
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
if (!config.cancelUrl) {
|
|
39
|
+
throw new Error('cancelUrl is required');
|
|
40
|
+
}
|
|
41
|
+
if (config.cancelUrl.length > MAX_URL_LENGTH) {
|
|
42
|
+
throw new Error(`cancelUrl exceeds maximum length of ${MAX_URL_LENGTH} characters`);
|
|
43
|
+
}
|
|
44
|
+
// Enhanced URL validation with security checks
|
|
45
|
+
const environment = detectEnvironment();
|
|
46
|
+
const returnUrlValidation = validateReturnUrl(config.returnUrl, environment);
|
|
47
|
+
if (!returnUrlValidation.isValid) {
|
|
48
|
+
throw new Error(`returnUrl validation failed: ${returnUrlValidation.error}`);
|
|
49
|
+
}
|
|
50
|
+
const cancelUrlValidation = validateReturnUrl(config.cancelUrl, environment);
|
|
51
|
+
if (!cancelUrlValidation.isValid) {
|
|
52
|
+
throw new Error(`cancelUrl validation failed: ${cancelUrlValidation.error}`);
|
|
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 && !['L1', 'L2'].includes(config.defaultVerificationMode)) {
|
|
63
|
+
throw new Error('defaultVerificationMode must be L1 or L2');
|
|
64
|
+
}
|
|
65
|
+
if (config.mode && !['redirect', 'new-tab'].includes(config.mode)) {
|
|
66
|
+
throw new Error('mode must be redirect or new-tab');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Detect environment based on current URL
|
|
71
|
+
*/
|
|
72
|
+
function detectEnvironment() {
|
|
73
|
+
const hostname = window.location.hostname;
|
|
74
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.includes('.local')) {
|
|
75
|
+
return 'development';
|
|
76
|
+
}
|
|
77
|
+
if (hostname.includes('staging') || hostname.includes('stage')) {
|
|
78
|
+
return 'staging';
|
|
79
|
+
}
|
|
80
|
+
return 'production';
|
|
81
|
+
}
|
|
82
|
+
export function validateSessionId(sessionId) {
|
|
83
|
+
if (!sessionId) {
|
|
84
|
+
throw new Error('sessionId is required');
|
|
85
|
+
}
|
|
86
|
+
if (!UUID_V4_PATTERN.test(sessionId)) {
|
|
87
|
+
throw new Error('sessionId must be a valid UUID v4');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function validateChallengeAge(age) {
|
|
91
|
+
if (age !== undefined) {
|
|
92
|
+
if (age < MINIMUM_AGE) {
|
|
93
|
+
throw new Error(`challengeAge must be at least ${MINIMUM_AGE}`);
|
|
94
|
+
}
|
|
95
|
+
if (age > MAXIMUM_AGE) {
|
|
96
|
+
throw new Error(`challengeAge cannot exceed ${MAXIMUM_AGE}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// URL validation now handled by security.ts validateReturnUrl function
|
|
101
|
+
/**
|
|
102
|
+
* Generate signed state parameter with HMAC protection
|
|
103
|
+
* Uses client-side HMAC for tamper resistance and server-side verification
|
|
104
|
+
*/
|
|
105
|
+
export async function generateState(payload, environment) {
|
|
106
|
+
// Import crypto utilities
|
|
107
|
+
const { createSignedState } = await import('./crypto');
|
|
108
|
+
return createSignedState(payload, environment);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Parse and validate signed state parameter
|
|
112
|
+
*/
|
|
113
|
+
export async function parseState(state, environment) {
|
|
114
|
+
try {
|
|
115
|
+
// Import crypto utilities
|
|
116
|
+
const { parseSignedState } = await import('./crypto');
|
|
117
|
+
// Try parsing as signed state first (new format)
|
|
118
|
+
const signedPayload = await parseSignedState(state, environment);
|
|
119
|
+
if (signedPayload) {
|
|
120
|
+
// Validate payload structure
|
|
121
|
+
if (!signedPayload.merchantId || !signedPayload.sessionId ||
|
|
122
|
+
!signedPayload.returnUrl || !signedPayload.cancelUrl) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return signedPayload;
|
|
126
|
+
}
|
|
127
|
+
// Fallback to legacy base64 format for backwards compatibility
|
|
128
|
+
console.warn('SafePassage: Falling back to legacy state format - update your SDK');
|
|
129
|
+
const json = atob(state);
|
|
130
|
+
const payload = JSON.parse(json);
|
|
131
|
+
// Validate payload structure
|
|
132
|
+
if (!payload.merchantId || !payload.sessionId || !payload.returnUrl || !payload.cancelUrl) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
// Check timestamp expiration
|
|
136
|
+
const age = Date.now() - payload.timestamp;
|
|
137
|
+
if (age > STATE_EXPIRY_MS) {
|
|
138
|
+
console.warn('SafePassage: State parameter expired', { age, maxAge: STATE_EXPIRY_MS });
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return payload;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
console.warn('SafePassage: Failed to parse state parameter', error);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
package/package.json
CHANGED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useSafePassage } from '../hooks/useSafePassage';
|
|
3
|
-
const SafePassageVerification = ({ wsUrl, mode = 'enhanced_verification', maxReconnectAttempts = 5, reconnectDelay = 3000, frameRate = 10, imageQuality = 0.8, config_overrides, onSuccess, onFailure, onStateChange, customMessages, className = '', style = {} }) => {
|
|
4
|
-
const { error, feedback, processingStep, zoneTransition, qualityScore, videoRef, canvasRef, isSessionStarted } = useSafePassage({
|
|
5
|
-
wsUrl,
|
|
6
|
-
mode,
|
|
7
|
-
maxReconnectAttempts,
|
|
8
|
-
reconnectDelay,
|
|
9
|
-
frameRate,
|
|
10
|
-
imageQuality,
|
|
11
|
-
config_overrides
|
|
12
|
-
}, {
|
|
13
|
-
onSuccess,
|
|
14
|
-
onFailure,
|
|
15
|
-
onStateChange
|
|
16
|
-
}, customMessages);
|
|
17
|
-
const getDisplayMessage = () => {
|
|
18
|
-
if (zoneTransition) {
|
|
19
|
-
return zoneTransition.message;
|
|
20
|
-
}
|
|
21
|
-
if (feedback && !processingStep) {
|
|
22
|
-
return feedback;
|
|
23
|
-
}
|
|
24
|
-
if (processingStep) {
|
|
25
|
-
return processingStep;
|
|
26
|
-
}
|
|
27
|
-
if (!isSessionStarted) {
|
|
28
|
-
return 'Starting camera...';
|
|
29
|
-
}
|
|
30
|
-
return 'Position your face in the camera and look forward';
|
|
31
|
-
};
|
|
32
|
-
const getMessageClass = () => {
|
|
33
|
-
if (zoneTransition)
|
|
34
|
-
return 'safepassage-instruction-zone-transition';
|
|
35
|
-
if (feedback && !processingStep)
|
|
36
|
-
return 'safepassage-instruction-feedback';
|
|
37
|
-
if (processingStep)
|
|
38
|
-
return 'safepassage-instruction-processing';
|
|
39
|
-
return 'safepassage-instruction-default';
|
|
40
|
-
};
|
|
41
|
-
// Calculate glow intensity based on quality score and feedback
|
|
42
|
-
const getGlowIntensity = () => {
|
|
43
|
-
// Base glow on quality score (0-1)
|
|
44
|
-
let intensity = qualityScore;
|
|
45
|
-
// Boost intensity for positive feedback (both generic and zone-specific)
|
|
46
|
-
if (feedback === 'good_position' ||
|
|
47
|
-
feedback === 'good_wide_position' ||
|
|
48
|
-
feedback === 'good_close_position') {
|
|
49
|
-
intensity = Math.max(intensity, 0.9);
|
|
50
|
-
}
|
|
51
|
-
// Medium intensity for zone positioning messages
|
|
52
|
-
if (feedback === 'position_at_comfortable_distance' ||
|
|
53
|
-
feedback === 'move_closer_to_camera') {
|
|
54
|
-
intensity = Math.max(intensity, 0.6);
|
|
55
|
-
}
|
|
56
|
-
// Reduce intensity for negative feedback
|
|
57
|
-
if (feedback === 'no_face_detected' ||
|
|
58
|
-
feedback === 'move_closer' ||
|
|
59
|
-
feedback === 'move_further' ||
|
|
60
|
-
feedback === 'center_face' ||
|
|
61
|
-
feedback === 'improve_lighting') {
|
|
62
|
-
intensity = Math.min(intensity, 0.3);
|
|
63
|
-
}
|
|
64
|
-
// Special handling for zone transitions - pulse effect
|
|
65
|
-
if (zoneTransition) {
|
|
66
|
-
intensity = Math.max(intensity, 0.7);
|
|
67
|
-
}
|
|
68
|
-
// Clamp between 0 and 1
|
|
69
|
-
return Math.max(0, Math.min(1, intensity));
|
|
70
|
-
};
|
|
71
|
-
const glowIntensity = getGlowIntensity();
|
|
72
|
-
return (_jsxs("div", { className: `safepassage-verification-container ${className}`, style: style, children: [_jsxs("div", { className: "safepassage-video-container", children: [_jsx("video", { ref: videoRef, autoPlay: true, playsInline: true, className: "safepassage-verification-video" }), _jsx("div", { className: "safepassage-video-overlay", children: _jsx("div", { className: "safepassage-oval-frame", style: {
|
|
73
|
-
'--glow-intensity': glowIntensity,
|
|
74
|
-
'--glow-opacity': glowIntensity > 0.1 ? glowIntensity : 0
|
|
75
|
-
} }) })] }), _jsx("div", { className: "safepassage-instructions-area", children: _jsx("div", { className: `safepassage-instruction-text ${getMessageClass()}`, children: getDisplayMessage() }) }), _jsx("canvas", { ref: canvasRef, style: { display: 'none' } }), error && (_jsx("div", { className: "safepassage-error-message", children: error })), _jsx("style", { children: `
|
|
76
|
-
.safepassage-verification-container {
|
|
77
|
-
display: flex;
|
|
78
|
-
flex-direction: column;
|
|
79
|
-
align-items: center;
|
|
80
|
-
justify-content: center;
|
|
81
|
-
width: 100%;
|
|
82
|
-
min-height: 100vh;
|
|
83
|
-
margin: 0;
|
|
84
|
-
padding: 40px 20px;
|
|
85
|
-
text-align: center;
|
|
86
|
-
background-color: #2a2a2a;
|
|
87
|
-
position: fixed;
|
|
88
|
-
top: 0;
|
|
89
|
-
left: 0;
|
|
90
|
-
right: 0;
|
|
91
|
-
bottom: 0;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
.safepassage-video-container {
|
|
95
|
-
position: relative;
|
|
96
|
-
width: 320px;
|
|
97
|
-
height: 320px;
|
|
98
|
-
margin-bottom: 40px;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.safepassage-verification-video {
|
|
102
|
-
width: 100%;
|
|
103
|
-
height: 100%;
|
|
104
|
-
object-fit: cover;
|
|
105
|
-
border-radius: 50%;
|
|
106
|
-
background-color: #000;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
.safepassage-video-overlay {
|
|
110
|
-
position: absolute;
|
|
111
|
-
top: 0;
|
|
112
|
-
left: 0;
|
|
113
|
-
width: 100%;
|
|
114
|
-
height: 100%;
|
|
115
|
-
pointer-events: none;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.safepassage-oval-frame {
|
|
119
|
-
width: 100%;
|
|
120
|
-
height: 100%;
|
|
121
|
-
border: 3px solid #ffffff;
|
|
122
|
-
border-radius: 50%;
|
|
123
|
-
box-shadow:
|
|
124
|
-
0 0 20px rgba(255, 255, 255, 0.3),
|
|
125
|
-
0 0 calc(30px * var(--glow-intensity, 0)) rgba(34, 197, 94, calc(var(--glow-opacity, 0) * 0.8)),
|
|
126
|
-
0 0 calc(50px * var(--glow-intensity, 0)) rgba(34, 197, 94, calc(var(--glow-opacity, 0) * 0.4)),
|
|
127
|
-
0 0 calc(80px * var(--glow-intensity, 0)) rgba(34, 197, 94, calc(var(--glow-opacity, 0) * 0.2));
|
|
128
|
-
transition: box-shadow 0.3s ease-out;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.safepassage-instructions-area {
|
|
132
|
-
width: 100%;
|
|
133
|
-
max-width: 400px;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
.safepassage-instruction-text {
|
|
137
|
-
font-size: 18px;
|
|
138
|
-
font-weight: 500;
|
|
139
|
-
color: #ffffff;
|
|
140
|
-
text-align: center;
|
|
141
|
-
line-height: 1.4;
|
|
142
|
-
min-height: 50px;
|
|
143
|
-
display: flex;
|
|
144
|
-
align-items: center;
|
|
145
|
-
justify-content: center;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
.safepassage-instruction-zone-transition {
|
|
149
|
-
color: #4fc3f7;
|
|
150
|
-
font-weight: 600;
|
|
151
|
-
animation: safepassage-pulse 0.5s ease-in-out;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
.safepassage-instruction-feedback {
|
|
155
|
-
color: #ffffff;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
.safepassage-instruction-processing {
|
|
159
|
-
color: #81c784;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
.safepassage-instruction-default {
|
|
163
|
-
color: #ffffff;
|
|
164
|
-
opacity: 0.9;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.safepassage-error-message {
|
|
168
|
-
color: #e53e3e;
|
|
169
|
-
margin-top: 10px;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
@keyframes safepassage-pulse {
|
|
173
|
-
0% { opacity: 0.7; transform: scale(1); }
|
|
174
|
-
50% { opacity: 1; transform: scale(1.02); }
|
|
175
|
-
100% { opacity: 0.9; transform: scale(1); }
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
@media (max-width: 480px) {
|
|
179
|
-
.safepassage-verification-container {
|
|
180
|
-
padding: 20px 15px;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
.safepassage-video-container {
|
|
184
|
-
width: 280px;
|
|
185
|
-
height: 280px;
|
|
186
|
-
margin-bottom: 30px;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
.safepassage-instruction-text {
|
|
190
|
-
font-size: 16px;
|
|
191
|
-
min-height: 45px;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
` })] }));
|
|
195
|
-
};
|
|
196
|
-
export default SafePassageVerification;
|