@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.
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@safepassage/sdk",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "SafePassage SDK - Lightweight redirect-based age verification",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,4 +0,0 @@
1
- import React from 'react';
2
- import type { SafePassageOptions } from '../types';
3
- declare const SafePassageVerification: React.FC<SafePassageOptions>;
4
- export default SafePassageVerification;
@@ -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;