SELF-808: Implement document camera MRZ demo screen (#1204)

This commit is contained in:
Justin Hernandez
2025-10-06 03:16:42 -07:00
committed by GitHub
parent bea9b7eff5
commit e4dab8b820
29 changed files with 1293 additions and 798 deletions

View File

@@ -117,7 +117,7 @@
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-sort-exports": "^0.9.1",
"jsdom": "^24.0.0",
"jsdom": "^25.0.1",
"prettier": "^3.5.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -126,7 +126,7 @@
"tamagui": "1.126.14",
"tsup": "^8.0.1",
"typescript": "^5.9.2",
"vitest": "^1.6.0"
"vitest": "^2.1.8"
},
"peerDependencies": {
"react": "^18.3.1",

View File

@@ -4,8 +4,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import type { DocumentCatalog, DocumentMetadata } from '@selfxyz/common/dist/esm/src/utils/types.js';
import type { IDDocument } from '@selfxyz/common/dist/esm/src/utils/types.js';
import type { DocumentCatalog, DocumentMetadata, IDDocument } from '@selfxyz/common/utils/types';
import { loadSelectedDocument, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import HomeScreen from './src/screens/HomeScreen';

View File

@@ -1,183 +0,0 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { describe, expect, it } from 'vitest';
import { computeHmac, pbkdf2, randomBytes, sha256, sha512 } from '../src/utils/ethers';
describe('Crypto Polyfills', () => {
describe('randomBytes', () => {
it('should generate random bytes of specified length', () => {
const bytes = randomBytes(32);
expect(bytes).toBeInstanceOf(Uint8Array);
expect(bytes.length).toBe(32);
});
it('should generate different random bytes on each call', () => {
const bytes1 = randomBytes(16);
const bytes2 = randomBytes(16);
expect(bytes1).not.toEqual(bytes2);
});
it('should handle different lengths', () => {
const bytes8 = randomBytes(8);
const bytes64 = randomBytes(64);
expect(bytes8.length).toBe(8);
expect(bytes64.length).toBe(64);
});
});
describe('sha256', () => {
it('should hash data correctly', () => {
const data = new Uint8Array([1, 2, 3, 4, 5]);
const hash = sha256(data);
expect(hash).toBeInstanceOf(Uint8Array);
expect(hash.length).toBe(32); // SHA-256 produces 32 bytes
});
it('should produce consistent hashes for same input', () => {
const data = new Uint8Array([1, 2, 3, 4, 5]);
const hash1 = sha256(data);
const hash2 = sha256(data);
expect(hash1).toEqual(hash2);
});
it('should produce different hashes for different inputs', () => {
const data1 = new Uint8Array([1, 2, 3, 4, 5]);
const data2 = new Uint8Array([1, 2, 3, 4, 6]);
const hash1 = sha256(data1);
const hash2 = sha256(data2);
expect(hash1).not.toEqual(hash2);
});
});
describe('sha512', () => {
it('should hash data correctly', () => {
const data = new Uint8Array([1, 2, 3, 4, 5]);
const hash = sha512(data);
expect(hash).toBeInstanceOf(Uint8Array);
expect(hash.length).toBe(64); // SHA-512 produces 64 bytes
});
it('should produce consistent hashes for same input', () => {
const data = new Uint8Array([1, 2, 3, 4, 5]);
const hash1 = sha512(data);
const hash2 = sha512(data);
expect(hash1).toEqual(hash2);
});
it('should produce different hashes for different inputs', () => {
const data1 = new Uint8Array([1, 2, 3, 4, 5]);
const data2 = new Uint8Array([1, 2, 3, 4, 6]);
const hash1 = sha512(data1);
const hash2 = sha512(data2);
expect(hash1).not.toEqual(hash2);
});
});
describe('computeHmac', () => {
it('should compute HMAC-SHA256 correctly', () => {
const key = new Uint8Array([1, 2, 3, 4]);
const data = new Uint8Array([5, 6, 7, 8]);
const hmac = computeHmac('sha256', key, data);
expect(hmac).toBeInstanceOf(Uint8Array);
expect(hmac.length).toBe(32); // HMAC-SHA256 produces 32 bytes
});
it('should compute HMAC-SHA512 correctly', () => {
const key = new Uint8Array([1, 2, 3, 4]);
const data = new Uint8Array([5, 6, 7, 8]);
const hmac = computeHmac('sha512', key, data);
expect(hmac).toBeInstanceOf(Uint8Array);
expect(hmac.length).toBe(64); // HMAC-SHA512 produces 64 bytes
});
it('should produce consistent HMAC for same inputs', () => {
const key = new Uint8Array([1, 2, 3, 4]);
const data = new Uint8Array([5, 6, 7, 8]);
const hmac1 = computeHmac('sha256', key, data);
const hmac2 = computeHmac('sha256', key, data);
expect(hmac1).toEqual(hmac2);
});
it('should produce different HMAC for different keys', () => {
const key1 = new Uint8Array([1, 2, 3, 4]);
const key2 = new Uint8Array([1, 2, 3, 5]);
const data = new Uint8Array([5, 6, 7, 8]);
const hmac1 = computeHmac('sha256', key1, data);
const hmac2 = computeHmac('sha256', key2, data);
expect(hmac1).not.toEqual(hmac2);
});
});
describe('pbkdf2', () => {
it('should derive key using PBKDF2-SHA256', () => {
const password = new Uint8Array([1, 2, 3, 4]);
const salt = new Uint8Array([5, 6, 7, 8]);
const key = pbkdf2(password, salt, 1000, 32, 'sha256');
expect(key).toBeInstanceOf(Uint8Array);
expect(key.length).toBe(32);
});
it('should derive key using PBKDF2-SHA512', () => {
const password = new Uint8Array([1, 2, 3, 4]);
const salt = new Uint8Array([5, 6, 7, 8]);
const key = pbkdf2(password, salt, 1000, 64, 'sha512');
expect(key).toBeInstanceOf(Uint8Array);
expect(key.length).toBe(64);
});
it('should produce consistent keys for same inputs', () => {
const password = new Uint8Array([1, 2, 3, 4]);
const salt = new Uint8Array([5, 6, 7, 8]);
const key1 = pbkdf2(password, salt, 1000, 32, 'sha256');
const key2 = pbkdf2(password, salt, 1000, 32, 'sha256');
expect(key1).toEqual(key2);
});
it('should produce different keys for different salts', () => {
const password = new Uint8Array([1, 2, 3, 4]);
const salt1 = new Uint8Array([5, 6, 7, 8]);
const salt2 = new Uint8Array([5, 6, 7, 9]);
const key1 = pbkdf2(password, salt1, 1000, 32, 'sha256');
const key2 = pbkdf2(password, salt2, 1000, 32, 'sha256');
expect(key1).not.toEqual(key2);
});
it('should handle different iteration counts', () => {
const password = new Uint8Array([1, 2, 3, 4]);
const salt = new Uint8Array([5, 6, 7, 8]);
const key1 = pbkdf2(password, salt, 1000, 32, 'sha256');
const key2 = pbkdf2(password, salt, 2000, 32, 'sha256');
expect(key1).not.toEqual(key2);
});
});
describe('ethers integration', () => {
it('should have ethers.randomBytes registered', () => {
// This test verifies that ethers.js is using our polyfill
const { ethers } = require('ethers');
expect(typeof ethers.randomBytes).toBe('function');
const bytes = ethers.randomBytes(16);
expect(bytes).toBeInstanceOf(Uint8Array);
expect(bytes.length).toBe(16);
});
});
});

View File

@@ -2,6 +2,7 @@
"name": "mobile-sdk-demo",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "index.js",
"scripts": {
"analyze:bundle:android": "yarn prebuild && node scripts/bundle-analyze-ci.cjs android",
@@ -9,7 +10,7 @@
"preandroid": "yarn prebuild",
"android": "react-native run-android --verbose",
"prebuild": "yarn workspace @selfxyz/mobile-sdk-alpha build",
"build": "tsc -p tsconfig.json --noEmit --pretty false",
"build": "yarn prebuild && tsc -p tsconfig.json --noEmit --pretty false",
"clean": "rm -rf ios/build android/app/build android/build && cd android && ./gradlew clean && cd ..",
"format": "prettier --write .",
"ia": "yarn install-app",
@@ -23,7 +24,7 @@
"start": "react-native start",
"test": "vitest run",
"test:watch": "vitest",
"types": "tsc --noEmit"
"types": "yarn prebuild && tsc --noEmit"
},
"dependencies": {
"@babel/runtime": "^7.28.3",

View File

@@ -0,0 +1,148 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { buildValidationRows, humanizeDocumentType, type NormalizedMRZResult } from '../utils/camera';
interface Props {
result: NormalizedMRZResult;
}
export default function DocumentScanResultCard({ result }: Props) {
const validationRows = useMemo(() => buildValidationRows(result.info.validation), [result]);
return (
<View style={styles.resultCard} accessible accessibilityRole="summary">
<Text style={styles.resultTitle}>Scan summary</Text>
<View style={styles.resultRow}>
<Text style={styles.resultLabel}>Document number</Text>
<Text style={styles.resultValue}>{result.info.documentNumber}</Text>
</View>
<View style={styles.resultRow}>
<Text style={styles.resultLabel}>Document type</Text>
<Text style={styles.resultValue}>{humanizeDocumentType(result.info.documentType)}</Text>
</View>
<View style={styles.resultRow}>
<Text style={styles.resultLabel}>Issuing country</Text>
<Text style={styles.resultValue}>{result.info.issuingCountry || 'Unknown'}</Text>
</View>
<View style={styles.resultRow}>
<Text style={styles.resultLabel}>Date of birth</Text>
<Text style={styles.resultValue}>{result.readableBirthDate}</Text>
</View>
<View style={styles.resultRow}>
<Text style={styles.resultLabel}>Expiry date</Text>
<Text style={styles.resultValue}>{result.readableExpiryDate}</Text>
</View>
<View style={styles.validationSection}>
<Text style={styles.validationTitle}>Validation checks</Text>
{validationRows ? (
validationRows.map(row => (
<View key={row.label} style={styles.validationRow}>
<Text style={styles.validationLabel}>{row.label}</Text>
<Text
style={[styles.validationBadge, row.value ? styles.validationPass : styles.validationFail]}
accessibilityRole="text"
>
{row.value ? '✓ Pass' : '✗ Fail'}
</Text>
</View>
))
) : (
<Text style={styles.validationPlaceholder}>Validation details are not available for this scan yet.</Text>
)}
</View>
</View>
);
}
const styles = StyleSheet.create({
resultCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
borderWidth: 1,
borderColor: '#e2e8f0',
marginBottom: 16,
shadowColor: '#0f172a',
shadowOpacity: 0.08,
shadowOffset: { width: 0, height: 2 },
shadowRadius: 6,
elevation: 2,
},
resultTitle: {
fontSize: 16,
fontWeight: '700',
color: '#0f172a',
marginBottom: 12,
},
resultRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
},
resultLabel: {
color: '#334155',
fontWeight: '500',
fontSize: 14,
},
resultValue: {
color: '#0f172a',
fontSize: 14,
fontWeight: '600',
},
validationSection: {
marginTop: 12,
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
},
validationTitle: {
fontSize: 15,
fontWeight: '600',
color: '#0f172a',
marginBottom: 8,
},
validationRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 6,
},
validationLabel: {
color: '#1f2937',
fontSize: 14,
flex: 1,
marginRight: 12,
},
validationBadge: {
minWidth: 90,
textAlign: 'center',
paddingVertical: 4,
paddingHorizontal: 8,
borderRadius: 999,
fontWeight: '600',
fontSize: 12,
color: '#ffffff',
},
validationPass: {
backgroundColor: '#16a34a',
},
validationFail: {
backgroundColor: '#b91c1c',
},
validationPlaceholder: {
color: '#475569',
fontSize: 13,
fontStyle: 'italic',
},
});

View File

@@ -4,7 +4,7 @@
import { useCallback, useEffect, useState } from 'react';
import type { DocumentCatalog, DocumentMetadata, IDDocument } from '@selfxyz/common/dist/esm/src/utils/types.js';
import type { DocumentCatalog, DocumentMetadata, IDDocument } from '@selfxyz/common/utils/types';
import { getAllDocuments, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { updateAfterDelete } from '../lib/catalog';

View File

@@ -0,0 +1,182 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AccessibilityInfo, PermissionsAndroid, Platform } from 'react-native';
import type { MRZInfo } from '@selfxyz/mobile-sdk-alpha';
import { useReadMRZ } from '@selfxyz/mobile-sdk-alpha/onboarding/read-mrz';
import { normalizeMRZPayload, type NormalizedMRZResult } from '../utils/camera';
type PermissionState = 'loading' | 'granted' | 'denied';
type ScanState = 'idle' | 'scanning' | 'success' | 'error';
function announceForAccessibility(message: string) {
if (!message) {
return;
}
try {
AccessibilityInfo.announceForAccessibility?.(message);
} catch {
// Intentionally swallow to avoid crashing accessibility users on announce failures.
}
}
export interface DocumentScannerCopy {
instructions: string;
success: string;
error: string;
permissionDenied: string;
resetAnnouncement: string;
}
export interface DocumentScannerState {
permissionStatus: PermissionState;
scanState: ScanState;
mrzResult: NormalizedMRZResult | null;
error: string | null;
requestPermission: () => Promise<void>;
handleMRZDetected: (payload: MRZInfo) => void;
handleScannerError: (error: string) => void;
handleScanAgain: () => void;
}
export function useMRZScanner(copy: DocumentScannerCopy): DocumentScannerState {
const [permissionStatus, setPermissionStatus] = useState<PermissionState>('loading');
const [scanState, setScanState] = useState<ScanState>('idle');
const [mrzResult, setMrzResult] = useState<NormalizedMRZResult | null>(null);
const [error, setError] = useState<string | null>(null);
const scanStartTimeRef = useRef<number>(Date.now());
const { onPassportRead } = useReadMRZ(scanStartTimeRef);
const requestPermission = useCallback(async () => {
setPermissionStatus('loading');
setError(null);
if (Platform.OS === 'android') {
try {
const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA, {
title: 'Camera permission',
message: 'We need your permission to access the camera for MRZ scanning.',
buttonPositive: 'Allow',
buttonNegative: 'Cancel',
buttonNeutral: 'Ask me later',
});
if (result === PermissionsAndroid.RESULTS.GRANTED) {
setPermissionStatus('granted');
} else {
setPermissionStatus('denied');
}
} catch {
setPermissionStatus('denied');
setError('Camera permission request failed. Please try again.');
}
} else {
setPermissionStatus('granted');
}
}, []);
useEffect(() => {
requestPermission();
}, [requestPermission]);
useEffect(() => {
if (permissionStatus === 'granted') {
announceForAccessibility(copy.instructions);
setScanState(current => {
if (current === 'success') {
return current;
}
scanStartTimeRef.current = Date.now();
return 'scanning';
});
} else if (permissionStatus === 'denied') {
announceForAccessibility(copy.permissionDenied);
setScanState('idle');
}
}, [copy.instructions, copy.permissionDenied, permissionStatus]);
useEffect(() => {
if (scanState === 'success') {
announceForAccessibility(copy.success);
} else if (scanState === 'error') {
announceForAccessibility(copy.error);
}
}, [copy.error, copy.success, scanState]);
useEffect(() => {
if (error) {
announceForAccessibility(error);
}
}, [error]);
const handleMRZDetected = useCallback(
(payload: MRZInfo) => {
setError(null);
setScanState(current => {
if (current === 'success') {
return current;
}
return 'scanning';
});
try {
const normalized = normalizeMRZPayload(payload);
setMrzResult(normalized);
setScanState('success');
onPassportRead(null, normalized.info);
} catch {
setScanState('error');
setError('Unable to validate the MRZ data from the scan.');
}
},
[onPassportRead],
);
const handleScannerError = useCallback((scannerError: string) => {
setScanState('error');
setError(scannerError || 'An unexpected camera error occurred.');
}, []);
const handleScanAgain = useCallback(() => {
if (permissionStatus === 'denied') {
requestPermission();
return;
}
scanStartTimeRef.current = Date.now();
setMrzResult(null);
setError(null);
setScanState('scanning');
announceForAccessibility(copy.resetAnnouncement);
}, [copy.resetAnnouncement, permissionStatus, requestPermission]);
return useMemo(
() => ({
permissionStatus,
scanState,
mrzResult,
error,
requestPermission,
handleMRZDetected,
handleScannerError,
handleScanAgain,
}),
[
error,
handleMRZDetected,
handleScanAgain,
handleScannerError,
mrzResult,
permissionStatus,
requestPermission,
scanState,
],
);
}

View File

@@ -4,7 +4,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type { IDDocument } from '@selfxyz/common/dist/esm/src/utils/types.js';
import type { IDDocument } from '@selfxyz/common/utils/types';
import { SdkEvents, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
export type RegistrationState = {

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import type { DocumentCatalog } from '@selfxyz/common/dist/esm/src/utils/types.js';
import type { DocumentCatalog } from '@selfxyz/common/utils/types';
export function updateAfterDelete(catalog: DocumentCatalog, deletedId: string): DocumentCatalog {
const remaining = (catalog.documents || []).filter(doc => doc.id !== deletedId);

View File

@@ -2,26 +2,233 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React from 'react';
import React, { useCallback } from 'react';
import { ActivityIndicator, Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import PlaceholderScreen from '../components/PlaceholderScreen';
import { MRZScannerView } from '@selfxyz/mobile-sdk-alpha/onboarding/read-mrz';
import ScreenLayout from '../components/ScreenLayout';
import DocumentScanResultCard from '../components/DocumentScanResultCard';
import { useMRZScanner } from '../hooks/useMRZScanner';
type Props = {
onBack: () => void;
};
const instructionsText = 'Align the machine-readable text with the frame and hold steady while we scan.';
const successMessage = 'Document scan successful. Review the details below.';
const errorMessage = 'We could not read your document. Adjust lighting and try again.';
const permissionDeniedMessage = 'Camera access was denied. Enable permissions to scan your document.';
export default function DocumentCamera({ onBack }: Props) {
const scannerCopy = {
instructions: instructionsText,
success: successMessage,
error: errorMessage,
permissionDenied: permissionDeniedMessage,
resetAnnouncement: 'Ready to scan again. Align the document in the viewfinder.',
} as const;
const {
permissionStatus,
scanState,
mrzResult,
error,
requestPermission,
handleMRZDetected,
handleScannerError,
handleScanAgain,
} = useMRZScanner(scannerCopy);
const handleSaveDocument = useCallback(() => {
if (!mrzResult) {
Alert.alert('Save Document', 'Scan a document before attempting to save.');
return;
}
Alert.alert(
'Save Document',
'Document storage will be available in a future release. Your scan is ready when you need it.',
);
}, [mrzResult]);
const renderPermissionDenied = () => (
<View style={styles.centeredState}>
<Text style={styles.permissionText}>{permissionDeniedMessage}</Text>
<TouchableOpacity accessibilityRole="button" style={styles.secondaryButton} onPress={requestPermission}>
<Text style={styles.secondaryButtonText}>Request Permission</Text>
</TouchableOpacity>
</View>
);
const renderLoading = () => (
<View style={styles.centeredState}>
<ActivityIndicator accessibilityLabel="Loading camera" color="#0f172a" />
<Text style={styles.statusText}>Preparing camera</Text>
</View>
);
return (
<PlaceholderScreen
<ScreenLayout
title="Document Camera"
onBack={onBack}
description="This screen would handle camera-based document scanning for passports and ID cards."
features={[
'Camera integration for document scanning',
'MRZ (Machine Readable Zone) detection',
'Document validation and parsing',
'Real-time feedback and guidance',
]}
/>
onBack={() => {
onBack();
}}
contentStyle={styles.screenContent}
rightAction={
<TouchableOpacity accessibilityRole="button" onPress={handleSaveDocument}>
<Text style={styles.headerAction}>Save Document</Text>
</TouchableOpacity>
}
>
{permissionStatus === 'loading' && renderLoading()}
{permissionStatus === 'denied' && renderPermissionDenied()}
{permissionStatus === 'granted' && (
<View style={styles.contentWrapper}>
<View style={styles.cameraWrapper}>
<MRZScannerView style={styles.scanner} onMRZDetected={handleMRZDetected} onError={handleScannerError} />
<View style={styles.overlay} accessibilityLiveRegion="polite" pointerEvents="none">
<Text style={styles.overlayTitle}>Position your document</Text>
<Text style={styles.overlayText}>{instructionsText}</Text>
</View>
</View>
<View style={styles.statusContainer}>
{scanState === 'scanning' && !error && (
<View style={styles.statusRow}>
<ActivityIndicator accessibilityLabel="Scanning" color="#2563eb" size="small" />
<Text style={styles.statusText}>Scanning for MRZ data</Text>
</View>
)}
{scanState === 'success' && mrzResult && (
<Text style={[styles.statusText, styles.successText]}>{successMessage}</Text>
)}
{scanState === 'error' && error && <Text style={[styles.statusText, styles.errorText]}>{error}</Text>}
</View>
{mrzResult && <DocumentScanResultCard result={mrzResult} />}
<View style={styles.actions}>
<TouchableOpacity accessibilityRole="button" onPress={handleScanAgain} style={styles.secondaryButton}>
<Text style={styles.secondaryButtonText}>Scan Again</Text>
</TouchableOpacity>
<TouchableOpacity accessibilityRole="button" onPress={handleSaveDocument} style={styles.primaryButton}>
<Text style={styles.primaryButtonText}>Save Document</Text>
</TouchableOpacity>
</View>
</View>
)}
</ScreenLayout>
);
}
const styles = StyleSheet.create({
screenContent: {
gap: 16,
},
contentWrapper: {
flex: 1,
},
cameraWrapper: {
backgroundColor: '#0f172a',
borderRadius: 16,
overflow: 'hidden',
minHeight: 260,
marginBottom: 16,
},
scanner: {
flex: 1,
width: '100%',
height: '100%',
backgroundColor: '#0f172a',
},
overlay: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(15, 23, 42, 0.75)',
paddingHorizontal: 16,
paddingVertical: 12,
},
overlayTitle: {
color: '#f8fafc',
fontWeight: '600',
fontSize: 16,
marginBottom: 4,
},
overlayText: {
color: '#e2e8f0',
fontSize: 14,
},
statusContainer: {
marginBottom: 16,
},
statusRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
statusText: {
color: '#0f172a',
fontSize: 14,
marginTop: 8,
},
successText: {
color: '#15803d',
fontWeight: '600',
},
errorText: {
color: '#b91c1c',
fontWeight: '600',
},
actions: {
flexDirection: 'row',
gap: 12,
},
primaryButton: {
flex: 1,
backgroundColor: '#0f172a',
paddingVertical: 12,
borderRadius: 8,
alignItems: 'center',
},
primaryButtonText: {
color: '#ffffff',
fontSize: 15,
fontWeight: '600',
},
secondaryButton: {
flex: 1,
backgroundColor: '#e2e8f0',
paddingVertical: 12,
borderRadius: 8,
alignItems: 'center',
},
secondaryButtonText: {
color: '#0f172a',
fontSize: 15,
fontWeight: '600',
},
headerAction: {
color: '#2563eb',
fontWeight: '600',
},
centeredState: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 12,
},
permissionText: {
color: '#0f172a',
textAlign: 'center',
fontSize: 15,
lineHeight: 22,
},
});

View File

@@ -5,7 +5,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import { ActivityIndicator, Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import type { DocumentCatalog } from '@selfxyz/common/dist/esm/src/utils/types.js';
import type { DocumentCatalog } from '@selfxyz/common/utils/types';
import { extractNameFromDocument, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import ScreenLayout from '../components/ScreenLayout';

View File

@@ -7,7 +7,7 @@ import { ActivityIndicator, Alert, Button, Platform, StyleSheet, Switch, Text, T
import { faker } from '@faker-js/faker';
import { calculateContentHash, inferDocumentCategory, isMRZDocument } from '@selfxyz/common';
import type { DocumentMetadata, IDDocument } from '@selfxyz/common/dist/esm/src/utils/types.js';
import type { DocumentMetadata, IDDocument } from '@selfxyz/common/utils/types';
import { generateMockDocument, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import SafeAreaScrollView from '../components/SafeAreaScrollView';

View File

@@ -5,7 +5,7 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { ActivityIndicator, Alert, Button, ScrollView, StyleSheet, Text, View } from 'react-native';
import type { DocumentCatalog, IDDocument } from '@selfxyz/common/dist/esm/src/utils/types.js';
import type { DocumentCatalog, IDDocument } from '@selfxyz/common/utils/types';
import { extractNameFromDocument, getAllDocuments, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { PickerField } from '../components/PickerField';

View File

@@ -4,7 +4,7 @@
import type { ComponentType } from 'react';
import type { DocumentCatalog, DocumentMetadata, IDDocument } from '@selfxyz/common/dist/esm/src/utils/types.js';
import type { DocumentCatalog, DocumentMetadata, IDDocument } from '@selfxyz/common/utils/types';
export type ScreenId = 'generate' | 'register' | 'prove' | 'camera' | 'nfc' | 'documents';

View File

@@ -0,0 +1,120 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { type MRZInfo, type MRZValidation, extractMRZInfo } from '@selfxyz/mobile-sdk-alpha';
export type MRZPayload = MRZInfo | (MRZInfo & { rawMRZ?: string; raw?: string; mrzString?: string }) | string;
export interface NormalizedMRZResult {
info: MRZInfo;
readableBirthDate: string;
readableExpiryDate: string;
}
export interface ValidationRow {
label: string;
value: boolean | undefined | null;
}
export function humanizeDocumentType(documentType: string): string {
if (documentType === 'P') {
return 'Passport';
}
if (documentType === 'I') {
return 'ID Card';
}
if (!documentType) {
return 'Unknown';
}
return documentType.trim().toUpperCase();
}
export function buildValidationRows(validation?: MRZValidation): ValidationRow[] | null {
if (!validation) {
return null;
}
return [
{ label: 'Format', value: validation.format },
{ label: 'Document number checksum', value: validation.passportNumberChecksum },
{ label: 'Date of birth checksum', value: validation.dateOfBirthChecksum },
{ label: 'Expiry date checksum', value: validation.dateOfExpiryChecksum },
{ label: 'Composite checksum', value: validation.compositeChecksum },
{ label: 'Overall validation', value: validation.overall },
];
}
export function formatMRZDate(mrzDate: string, locale: string = 'default'): string {
if (!/^\d{6}$/.test(mrzDate)) {
return 'Unknown';
}
const yearPart = parseInt(mrzDate.slice(0, 2), 10);
const monthPart = parseInt(mrzDate.slice(2, 4), 10);
const dayPart = parseInt(mrzDate.slice(4, 6), 10);
if (Number.isNaN(yearPart) || monthPart < 1 || monthPart > 12 || dayPart < 1 || dayPart > 31) {
return 'Unknown';
}
const currentYear = new Date().getFullYear() % 100;
const century = yearPart > currentYear ? 1900 : 2000;
const fullYear = century + yearPart;
const date = new Date(Date.UTC(fullYear, monthPart - 1, dayPart));
if (Number.isNaN(date.getTime())) {
return 'Unknown';
}
try {
if (typeof Intl !== 'undefined') {
const localeOption = locale === 'default' ? undefined : locale;
return new Intl.DateTimeFormat(localeOption, {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC',
}).format(date);
}
} catch {
// Fallback to ISO-like format below.
}
const monthString = `${monthPart}`.padStart(2, '0');
const dayString = `${dayPart}`.padStart(2, '0');
return `${fullYear}-${monthString}-${dayString}`;
}
export function normalizeMRZPayload(payload: MRZPayload): NormalizedMRZResult {
let info: MRZInfo;
if (typeof payload === 'string') {
info = extractMRZInfo(payload);
} else {
const withRaw = payload as MRZInfo & { rawMRZ?: string; raw?: string; mrzString?: string };
const rawCandidate = withRaw.rawMRZ ?? withRaw.raw ?? withRaw.mrzString;
if (typeof rawCandidate === 'string' && rawCandidate.trim().length > 0) {
info = extractMRZInfo(rawCandidate);
} else {
info = {
documentNumber: withRaw.documentNumber,
dateOfBirth: withRaw.dateOfBirth,
dateOfExpiry: withRaw.dateOfExpiry,
issuingCountry: withRaw.issuingCountry,
documentType: withRaw.documentType,
validation: withRaw.validation,
};
}
}
return {
info,
readableBirthDate: formatMRZDate(info.dateOfBirth),
readableExpiryDate: formatMRZDate(info.dateOfExpiry),
};
}

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import type { DocumentMetadata } from '@selfxyz/common/dist/esm/src/utils/types.js';
import type { DocumentMetadata } from '@selfxyz/common/utils/types';
export function humanizeDocumentType(documentType: string): string {
const toTitle = (s: string) => s.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase());

View File

@@ -5,7 +5,7 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import type { DocumentsAdapter } from '@selfxyz/mobile-sdk-alpha';
import type { DocumentCatalog, IDDocument, PassportData } from '@selfxyz/common/dist/esm/src/utils/types.js';
import type { DocumentCatalog, IDDocument, PassportData } from '@selfxyz/common/utils/types';
import { getSKIPEM, initPassportDataParsing } from '@selfxyz/common';
const CATALOG_KEY = '@self_demo:document_catalog';

View File

@@ -1,242 +0,0 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
/**
* Tests for crypto-polyfill.js demonstrating functional bugs:
* 1. Method chaining breaks due to incorrect `this` binding
* 2. RNG import fails - react-native-get-random-values doesn't export getRandomValues
* 3. Buffer polyfill missing
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Preserve original crypto
const originalCrypto = global.crypto;
// Mock crypto.getRandomValues in jsdom environment
if (typeof global.crypto === 'undefined' || !global.crypto.getRandomValues) {
const mockGetRandomValues = vi.fn((array: Uint8Array) => {
// Fill with predictable values for testing
for (let i = 0; i < array.length; i++) {
array[i] = i % 256;
}
return array;
});
Object.defineProperty(global, 'crypto', {
value: {
getRandomValues: mockGetRandomValues,
},
writable: true,
configurable: true,
});
}
// Mock Buffer globally to simulate React Native environment where Buffer is undefined
const originalBuffer = global.Buffer;
describe('Crypto Polyfill Functional Bugs', () => {
let crypto: any;
beforeEach(() => {
// Clear module cache to get fresh instance
vi.resetModules();
vi.restoreAllMocks();
vi.clearAllMocks();
});
afterEach(() => {
// Restore Buffer if we removed it
global.Buffer = originalBuffer;
// Restore crypto (use Object.defineProperty for read-only properties)
if (originalCrypto) {
Object.defineProperty(global, 'crypto', {
value: originalCrypto,
writable: true,
configurable: true,
});
}
});
describe('Method Chaining Bug', () => {
it('should allow method chaining with update() calls', async () => {
crypto = await import('../src/polyfills/cryptoPolyfill.js');
// This should work but currently fails due to `this` binding issue
expect(() => {
const hasher = crypto.createHash('sha256');
const result = hasher.update('Hello ').update('World').digest('hex');
expect(typeof result).toBe('string');
expect(result.length).toBe(64); // SHA256 hex length
}).not.toThrow();
});
it('should return the hasher instance from update() for chaining', async () => {
crypto = await import('../src/polyfills/cryptoPolyfill.js');
const hasher = crypto.createHash('sha256');
const updateResult = hasher.update('test');
// This should be the same object for chaining
expect(updateResult).toBe(hasher);
expect(updateResult.update).toBeInstanceOf(Function);
expect(updateResult.digest).toBeInstanceOf(Function);
});
it('should produce the same result for chained vs separate calls', async () => {
crypto = await import('../src/polyfills/cryptoPolyfill.js');
// Chained approach
const chainedResult = crypto.createHash('sha256').update('Hello ').update('World').digest('hex');
// Separate calls approach
const hasher = crypto.createHash('sha256');
hasher.update('Hello ');
hasher.update('World');
const separateResult = hasher.digest('hex');
expect(chainedResult).toBe(separateResult);
});
});
describe('RNG Import Bug', () => {
it('should not try to destructure getRandomValues from react-native-get-random-values', async () => {
// Mock the require to simulate the actual package behavior
vi.doMock('react-native-get-random-values', () => {
// This package doesn't export getRandomValues - it just polyfills globalThis.crypto
global.crypto = global.crypto || ({} as typeof crypto);
global.crypto.getRandomValues = vi.fn((array: Uint8Array) => {
for (let i = 0; i < array.length; i++) {
array[i] = i % 256;
}
return array;
}) as any;
return {}; // Empty export
});
// Should now work because we use globalThis.crypto.getRandomValues, not destructuring
crypto = await import('../src/polyfills/cryptoPolyfill.js');
const result = crypto.randomBytes(16);
expect(result).toBeInstanceOf(Buffer);
expect(result.length).toBe(16);
});
it('should throw helpful error when crypto.getRandomValues is not available', async () => {
// Clear module cache and remove crypto polyfill
vi.resetModules();
vi.doMock('react-native-get-random-values', () => {
// Mock a broken polyfill that doesn't install crypto
return {};
});
// Remove crypto to simulate missing polyfill
const originalCrypto = global.crypto;
delete (global as any).crypto;
await expect(async () => {
crypto = await import('../src/polyfills/cryptoPolyfill.js');
crypto.randomBytes(16);
}).rejects.toThrow(/globalThis.crypto.getRandomValues is not available/);
global.crypto = originalCrypto;
});
});
describe('Buffer Polyfill Bug', () => {
it('should gracefully handle Buffer availability check', async () => {
// This test verifies that the crypto polyfill checks for Buffer availability
// Note: We can't actually delete Buffer because Vitest's worker threads need it
// Instead, we verify that the polyfill works correctly with and without Buffer
// Import the polyfill normally
crypto = await import('../src/polyfills/cryptoPolyfill.js');
// Test that randomBytes returns a typed array
const result = crypto.randomBytes(32);
// The result should be either a Buffer or Uint8Array (Buffer extends Uint8Array)
// Buffer is available in Node.js environment, so we expect Buffer here
expect(result).toBeInstanceOf(Buffer);
expect(result.length).toBe(32);
// Verify it's a valid typed array with proper values
expect(result.every((byte: number) => byte >= 0 && byte <= 255)).toBe(true);
});
it('should work with Buffer polyfill imported', async () => {
// Reset mocks for this test
vi.unmock('buffer');
vi.resetModules();
vi.restoreAllMocks();
// Simulate proper Buffer polyfill
global.Buffer = require('buffer').Buffer;
crypto = await import('../src/polyfills/cryptoPolyfill.js');
const result = crypto.createHash('sha256').update('test').digest('hex');
expect(typeof result).toBe('string');
expect(result.length).toBe(64);
});
it('should handle different data types correctly with Buffer polyfill', async () => {
// Reset mocks for this test
vi.unmock('buffer');
vi.resetModules();
vi.restoreAllMocks();
global.Buffer = require('buffer').Buffer;
crypto = await import('../src/polyfills/cryptoPolyfill.js');
const hasher = crypto.createHash('sha256');
// Should handle strings
hasher.update('string data');
// Should handle Uint8Array
hasher.update(new Uint8Array([1, 2, 3, 4]));
// Should handle Buffer
hasher.update(Buffer.from('buffer data'));
const result = hasher.digest('hex');
expect(typeof result).toBe('string');
expect(result.length).toBe(64);
});
});
describe('Integration Tests', () => {
it('should work end-to-end with all fixes applied', async () => {
// Reset mocks for this test
vi.unmock('buffer');
vi.resetModules();
vi.restoreAllMocks();
// Set up proper environment
global.Buffer = require('buffer').Buffer;
global.crypto = global.crypto || ({} as typeof crypto);
global.crypto.getRandomValues = vi.fn((array: Uint8Array) => {
for (let i = 0; i < array.length; i++) {
array[i] = i % 256;
}
return array;
}) as any;
crypto = await import('../src/polyfills/cryptoPolyfill.js');
// Test hash chaining
const hashResult = crypto.createHash('sha256').update('Hello ').update('World').digest('hex');
expect(typeof hashResult).toBe('string');
expect(hashResult.length).toBe(64);
// Test randomBytes
const randomResult = crypto.randomBytes(32);
expect(randomResult).toBeInstanceOf(Buffer);
expect(randomResult.length).toBe(32);
});
});
});

View File

@@ -0,0 +1,421 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
/**
* Comprehensive tests for crypto polyfills including:
* 1. Functional bug tests for crypto-polyfill.js
* 2. Ethers integration tests for crypto utilities
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { computeHmac, pbkdf2, randomBytes, sha256, sha512 } from '../src/utils/ethers';
// Preserve original crypto
const originalCrypto = global.crypto;
// Mock crypto.getRandomValues in jsdom environment
if (typeof global.crypto === 'undefined' || !global.crypto.getRandomValues) {
const mockGetRandomValues = vi.fn((array: Uint8Array) => {
// Fill with predictable values for testing
for (let i = 0; i < array.length; i++) {
array[i] = i % 256;
}
return array;
});
Object.defineProperty(global, 'crypto', {
value: {
getRandomValues: mockGetRandomValues,
},
writable: true,
configurable: true,
});
}
// Mock Buffer globally to simulate React Native environment where Buffer is undefined
const originalBuffer = global.Buffer;
describe('Crypto Polyfills', () => {
let crypto: any;
beforeEach(() => {
// Clear module cache to get fresh instance
vi.resetModules();
vi.restoreAllMocks();
vi.clearAllMocks();
});
afterEach(() => {
// Restore Buffer if we removed it
global.Buffer = originalBuffer;
// Restore crypto (use Object.defineProperty for read-only properties)
if (originalCrypto) {
Object.defineProperty(global, 'crypto', {
value: originalCrypto,
writable: true,
configurable: true,
});
}
});
describe('Ethers Crypto Utilities', () => {
describe('randomBytes', () => {
it('should generate random bytes of specified length', () => {
const bytes = randomBytes(32);
expect(bytes).toBeInstanceOf(Uint8Array);
expect(bytes.length).toBe(32);
});
it('should generate different random bytes on each call', () => {
const bytes1 = randomBytes(16);
const bytes2 = randomBytes(16);
expect(bytes1).not.toEqual(bytes2);
});
it('should handle different lengths', () => {
const bytes8 = randomBytes(8);
const bytes64 = randomBytes(64);
expect(bytes8.length).toBe(8);
expect(bytes64.length).toBe(64);
});
});
describe('sha256', () => {
it('should hash data correctly', () => {
const data = new Uint8Array([1, 2, 3, 4, 5]);
const hash = sha256(data);
expect(hash).toBeInstanceOf(Uint8Array);
expect(hash.length).toBe(32); // SHA-256 produces 32 bytes
});
it('should produce consistent hashes for same input', () => {
const data = new Uint8Array([1, 2, 3, 4, 5]);
const hash1 = sha256(data);
const hash2 = sha256(data);
expect(hash1).toEqual(hash2);
});
it('should produce different hashes for different inputs', () => {
const data1 = new Uint8Array([1, 2, 3, 4, 5]);
const data2 = new Uint8Array([1, 2, 3, 4, 6]);
const hash1 = sha256(data1);
const hash2 = sha256(data2);
expect(hash1).not.toEqual(hash2);
});
});
describe('sha512', () => {
it('should hash data correctly', () => {
const data = new Uint8Array([1, 2, 3, 4, 5]);
const hash = sha512(data);
expect(hash).toBeInstanceOf(Uint8Array);
expect(hash.length).toBe(64); // SHA-512 produces 64 bytes
});
it('should produce consistent hashes for same input', () => {
const data = new Uint8Array([1, 2, 3, 4, 5]);
const hash1 = sha512(data);
const hash2 = sha512(data);
expect(hash1).toEqual(hash2);
});
it('should produce different hashes for different inputs', () => {
const data1 = new Uint8Array([1, 2, 3, 4, 5]);
const data2 = new Uint8Array([1, 2, 3, 4, 6]);
const hash1 = sha512(data1);
const hash2 = sha512(data2);
expect(hash1).not.toEqual(hash2);
});
});
describe('computeHmac', () => {
it('should compute HMAC-SHA256 correctly', () => {
const key = new Uint8Array([1, 2, 3, 4]);
const data = new Uint8Array([5, 6, 7, 8]);
const hmac = computeHmac('sha256', key, data);
expect(hmac).toBeInstanceOf(Uint8Array);
expect(hmac.length).toBe(32); // HMAC-SHA256 produces 32 bytes
});
it('should compute HMAC-SHA512 correctly', () => {
const key = new Uint8Array([1, 2, 3, 4]);
const data = new Uint8Array([5, 6, 7, 8]);
const hmac = computeHmac('sha512', key, data);
expect(hmac).toBeInstanceOf(Uint8Array);
expect(hmac.length).toBe(64); // HMAC-SHA512 produces 64 bytes
});
it('should produce consistent HMAC for same inputs', () => {
const key = new Uint8Array([1, 2, 3, 4]);
const data = new Uint8Array([5, 6, 7, 8]);
const hmac1 = computeHmac('sha256', key, data);
const hmac2 = computeHmac('sha256', key, data);
expect(hmac1).toEqual(hmac2);
});
it('should produce different HMAC for different keys', () => {
const key1 = new Uint8Array([1, 2, 3, 4]);
const key2 = new Uint8Array([1, 2, 3, 5]);
const data = new Uint8Array([5, 6, 7, 8]);
const hmac1 = computeHmac('sha256', key1, data);
const hmac2 = computeHmac('sha256', key2, data);
expect(hmac1).not.toEqual(hmac2);
});
});
describe('pbkdf2', () => {
it('should derive key using PBKDF2-SHA256', () => {
const password = new Uint8Array([1, 2, 3, 4]);
const salt = new Uint8Array([5, 6, 7, 8]);
const key = pbkdf2(password, salt, 1000, 32, 'sha256');
expect(key).toBeInstanceOf(Uint8Array);
expect(key.length).toBe(32);
});
it('should derive key using PBKDF2-SHA512', () => {
const password = new Uint8Array([1, 2, 3, 4]);
const salt = new Uint8Array([5, 6, 7, 8]);
const key = pbkdf2(password, salt, 1000, 64, 'sha512');
expect(key).toBeInstanceOf(Uint8Array);
expect(key.length).toBe(64);
});
it('should produce consistent keys for same inputs', () => {
const password = new Uint8Array([1, 2, 3, 4]);
const salt = new Uint8Array([5, 6, 7, 8]);
const key1 = pbkdf2(password, salt, 1000, 32, 'sha256');
const key2 = pbkdf2(password, salt, 1000, 32, 'sha256');
expect(key1).toEqual(key2);
});
it('should produce different keys for different salts', () => {
const password = new Uint8Array([1, 2, 3, 4]);
const salt1 = new Uint8Array([5, 6, 7, 8]);
const salt2 = new Uint8Array([5, 6, 7, 9]);
const key1 = pbkdf2(password, salt1, 1000, 32, 'sha256');
const key2 = pbkdf2(password, salt2, 1000, 32, 'sha256');
expect(key1).not.toEqual(key2);
});
it('should handle different iteration counts', () => {
const password = new Uint8Array([1, 2, 3, 4]);
const salt = new Uint8Array([5, 6, 7, 8]);
const key1 = pbkdf2(password, salt, 1000, 32, 'sha256');
const key2 = pbkdf2(password, salt, 2000, 32, 'sha256');
expect(key1).not.toEqual(key2);
});
});
describe('ethers integration', () => {
it('should have ethers.randomBytes registered', () => {
// This test verifies that ethers.js is using our polyfill
const { ethers } = require('ethers');
expect(typeof ethers.randomBytes).toBe('function');
const bytes = ethers.randomBytes(16);
expect(bytes).toBeInstanceOf(Uint8Array);
expect(bytes.length).toBe(16);
});
});
});
describe('Crypto Polyfill Functional Bugs', () => {
describe('Method Chaining Bug', () => {
it('should allow method chaining with update() calls', async () => {
crypto = await import('../src/polyfills/cryptoPolyfill.js');
// This should work but currently fails due to `this` binding issue
expect(() => {
const hasher = crypto.createHash('sha256');
const result = hasher.update('Hello ').update('World').digest('hex');
expect(typeof result).toBe('string');
expect(result.length).toBe(64); // SHA256 hex length
}).not.toThrow();
});
it('should return the hasher instance from update() for chaining', async () => {
crypto = await import('../src/polyfills/cryptoPolyfill.js');
const hasher = crypto.createHash('sha256');
const updateResult = hasher.update('test');
// This should be the same object for chaining
expect(updateResult).toBe(hasher);
expect(updateResult.update).toBeInstanceOf(Function);
expect(updateResult.digest).toBeInstanceOf(Function);
});
it('should produce the same result for chained vs separate calls', async () => {
crypto = await import('../src/polyfills/cryptoPolyfill.js');
// Chained approach
const chainedResult = crypto.createHash('sha256').update('Hello ').update('World').digest('hex');
// Separate calls approach
const hasher = crypto.createHash('sha256');
hasher.update('Hello ');
hasher.update('World');
const separateResult = hasher.digest('hex');
expect(chainedResult).toBe(separateResult);
});
});
describe('RNG Import Bug', () => {
it('should not try to destructure getRandomValues from react-native-get-random-values', async () => {
// Mock the require to simulate the actual package behavior
vi.doMock('react-native-get-random-values', () => {
// This package doesn't export getRandomValues - it just polyfills globalThis.crypto
global.crypto = global.crypto || ({} as typeof crypto);
global.crypto.getRandomValues = vi.fn((array: Uint8Array) => {
for (let i = 0; i < array.length; i++) {
array[i] = i % 256;
}
return array;
}) as any;
return {}; // Empty export
});
// Should now work because we use globalThis.crypto.getRandomValues, not destructuring
crypto = await import('../src/polyfills/cryptoPolyfill.js');
const result = crypto.randomBytes(16);
expect(result).toBeInstanceOf(Buffer);
expect(result.length).toBe(16);
});
it('should throw helpful error when crypto.getRandomValues is not available', async () => {
// Clear module cache and remove crypto polyfill
vi.resetModules();
vi.doMock('react-native-get-random-values', () => {
// Mock a broken polyfill that doesn't install crypto
return {};
});
// Remove crypto to simulate missing polyfill
const originalCrypto = global.crypto;
delete (global as any).crypto;
await expect(async () => {
crypto = await import('../src/polyfills/cryptoPolyfill.js');
crypto.randomBytes(16);
}).rejects.toThrow(/globalThis.crypto.getRandomValues is not available/);
global.crypto = originalCrypto;
});
});
describe('Buffer Polyfill Bug', () => {
it('should gracefully handle Buffer availability check', async () => {
// This test verifies that the crypto polyfill checks for Buffer availability
// Note: We can't actually delete Buffer because Vitest's worker threads need it
// Instead, we verify that the polyfill works correctly with and without Buffer
// Import the polyfill normally
crypto = await import('../src/polyfills/cryptoPolyfill.js');
// Test that randomBytes returns a typed array
const result = crypto.randomBytes(32);
// The result should be either a Buffer or Uint8Array (Buffer extends Uint8Array)
// Buffer is available in Node.js environment, so we expect Buffer here
expect(result).toBeInstanceOf(Buffer);
expect(result.length).toBe(32);
// Verify it's a valid typed array with proper values
expect(result.every((byte: number) => byte >= 0 && byte <= 255)).toBe(true);
});
it('should work with Buffer polyfill imported', async () => {
// Reset mocks for this test
vi.unmock('buffer');
vi.resetModules();
vi.restoreAllMocks();
// Simulate proper Buffer polyfill
global.Buffer = require('buffer').Buffer;
crypto = await import('../src/polyfills/cryptoPolyfill.js');
const result = crypto.createHash('sha256').update('test').digest('hex');
expect(typeof result).toBe('string');
expect(result.length).toBe(64);
});
it('should handle different data types correctly with Buffer polyfill', async () => {
// Reset mocks for this test
vi.unmock('buffer');
vi.resetModules();
vi.restoreAllMocks();
global.Buffer = require('buffer').Buffer;
crypto = await import('../src/polyfills/cryptoPolyfill.js');
const hasher = crypto.createHash('sha256');
// Should handle strings
hasher.update('string data');
// Should handle Uint8Array
hasher.update(new Uint8Array([1, 2, 3, 4]));
// Should handle Buffer
hasher.update(Buffer.from('buffer data'));
const result = hasher.digest('hex');
expect(typeof result).toBe('string');
expect(result.length).toBe(64);
});
});
describe('Integration Tests', () => {
it('should work end-to-end with all fixes applied', async () => {
// Reset mocks for this test
vi.unmock('buffer');
vi.resetModules();
vi.restoreAllMocks();
// Set up proper environment
global.Buffer = require('buffer').Buffer;
global.crypto = global.crypto || ({} as typeof crypto);
global.crypto.getRandomValues = vi.fn((array: Uint8Array) => {
for (let i = 0; i < array.length; i++) {
array[i] = i % 256;
}
return array;
}) as any;
crypto = await import('../src/polyfills/cryptoPolyfill.js');
// Test hash chaining
const hashResult = crypto.createHash('sha256').update('Hello ').update('World').digest('hex');
expect(typeof hashResult).toBe('string');
expect(hashResult.length).toBe(64);
// Test randomBytes
const randomResult = crypto.randomBytes(32);
expect(randomResult).toBeInstanceOf(Buffer);
expect(randomResult.length).toBe(32);
});
});
});
});

View File

@@ -12,7 +12,7 @@
import { describe, expect, it } from 'vitest';
import type { PassportData } from '@selfxyz/common/dist/esm/src/utils/types.js';
import type { PassportData } from '@selfxyz/common/utils/types';
describe('documentStore - BigInt serialization (simplified)', () => {
it('should demonstrate the BigInt serialization problem with JSON.stringify/parse', () => {

View File

@@ -4,7 +4,7 @@
import { describe, expect, it } from 'vitest';
import type { DocumentCatalog } from '@selfxyz/common/dist/esm/src/utils/types.js';
import type { DocumentCatalog } from '@selfxyz/common/utils/types';
import { selectDocument, updateAfterDelete } from '../../src/lib/catalog';

View File

@@ -0,0 +1,2 @@
export { extractMRZInfo, formatDateToYYMMDD } from '../../../mobile-sdk-alpha/src/processing/mrz';
export type { MRZInfo, MRZValidation } from '../../../mobile-sdk-alpha/src/types/public';

View File

@@ -0,0 +1,45 @@
export const AccessibilityInfo = {
announceForAccessibility: () => Promise.resolve(),
};
export const ActivityIndicator = () => null;
export const Alert = { alert: () => undefined };
export const PermissionsAndroid = {
PERMISSIONS: { CAMERA: 'android.permission.CAMERA' },
RESULTS: { GRANTED: 'granted', DENIED: 'denied' },
request: async () => 'granted' as const,
};
export const Platform = {
OS: 'ios',
select<T>(mapping: Record<string, T> & { default?: T }): T {
if (Object.prototype.hasOwnProperty.call(mapping, 'ios')) {
return mapping.ios as T;
}
if (Object.prototype.hasOwnProperty.call(mapping, 'default')) {
return mapping.default as T;
}
return Object.values(mapping)[0] as T;
},
};
export const StyleSheet = {
create: <T extends Record<string, object>>(styles: T) => styles,
};
export const Text = () => null;
export const TouchableOpacity = () => null;
export const View = () => null;
export default {
AccessibilityInfo,
ActivityIndicator,
Alert,
PermissionsAndroid,
Platform,
StyleSheet,
Text,
TouchableOpacity,
View,
};

View File

@@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { describe, expect, it } from 'vitest';
import type { MRZInfo } from '@selfxyz/mobile-sdk-alpha';
import { buildValidationRows, formatMRZDate, humanizeDocumentType, normalizeMRZPayload } from '../../src/utils/camera';
describe('formatMRZDate', () => {
it('formats valid YYMMDD strings into readable dates', () => {
expect(formatMRZDate('740812', 'en-US')).toBe('August 12, 1974');
expect(formatMRZDate('010101', 'en-US')).toBe('January 1, 2001');
});
it('returns Unknown for invalid values', () => {
expect(formatMRZDate('991332', 'en-US')).toBe('Unknown');
expect(formatMRZDate('abc123', 'en-US')).toBe('Unknown');
});
});
describe('normalizeMRZPayload', () => {
it('parses raw MRZ strings and surfaces validation data', () => {
const rawMRZ = 'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<<<<\nL898902C36UTO7408122F1204159ZE184226B<<<<<10';
const normalized = normalizeMRZPayload(rawMRZ);
expect(normalized.info.documentNumber).toBe('L898902C3');
expect(normalized.info.dateOfBirth).toBe('740812');
expect(normalized.info.dateOfExpiry).toBe('120415');
expect(normalized.info.validation?.overall).toBe(true);
});
it('preserves provided MRZ info when validation already exists', () => {
const info: MRZInfo = {
documentNumber: 'X1234567',
dateOfBirth: '010101',
dateOfExpiry: '251231',
issuingCountry: 'UTO',
documentType: 'P',
validation: {
format: true,
passportNumberChecksum: true,
dateOfBirthChecksum: true,
dateOfExpiryChecksum: true,
compositeChecksum: true,
overall: true,
},
};
const normalized = normalizeMRZPayload(info);
expect(normalized.info).toEqual(info);
expect(normalized.readableBirthDate).toBe(formatMRZDate('010101', 'en-US'));
});
});
describe('humanizeDocumentType', () => {
it('maps known document codes to friendly labels', () => {
expect(humanizeDocumentType('P')).toBe('Passport');
expect(humanizeDocumentType('I')).toBe('ID Card');
});
it('falls back to normalized text for unknown values', () => {
expect(humanizeDocumentType(' visa ')).toBe('VISA');
expect(humanizeDocumentType('')).toBe('Unknown');
});
});
describe('buildValidationRows', () => {
it('returns null when validation is unavailable', () => {
expect(buildValidationRows(undefined)).toBeNull();
});
it('maps MRZ validation flags into labelled rows', () => {
const rows = buildValidationRows({
format: true,
passportNumberChecksum: true,
dateOfBirthChecksum: false,
dateOfExpiryChecksum: true,
compositeChecksum: true,
overall: false,
});
expect(rows).toMatchObject([
{ label: 'Format', value: true },
{ label: 'Document number checksum', value: true },
{ label: 'Date of birth checksum', value: false },
{ label: 'Expiry date checksum', value: true },
{ label: 'Composite checksum', value: true },
{ label: 'Overall validation', value: false },
]);
});
});

View File

@@ -4,7 +4,7 @@
import { describe, expect, it } from 'vitest';
import type { DocumentMetadata } from '@selfxyz/common/dist/esm/src/utils/types.js';
import type { DocumentMetadata } from '@selfxyz/common/utils/types';
import { formatDataPreview, humanizeDocumentType, maskId } from '../../src/utils/document';

View File

@@ -1,9 +1,20 @@
{
"extends": "@tsconfig/react-native/tsconfig.json",
"compilerOptions": {
"module": "ES2020",
"strict": true,
"allowJs": true,
"baseUrl": ".",
"moduleResolution": "Bundler",
"lib": ["ES2020", "DOM"],
"skipLibCheck": true,
"paths": {
"@selfxyz/mobile-sdk-alpha": ["../../packages/mobile-sdk-alpha/dist/esm/index"],
"@selfxyz/mobile-sdk-alpha/onboarding/*": ["../../packages/mobile-sdk-alpha/dist/esm/flows/onboarding/*"],
"@selfxyz/mobile-sdk-alpha/disclosing/*": ["../../packages/mobile-sdk-alpha/dist/esm/flows/disclosing/*"]
},
"types": ["react-native", "vitest/globals"]
},
"include": ["src", "App.tsx", "index.js", "types/**/*", "__tests__/**/*", "tests/**/*"]
"include": ["src", "App.tsx", "index.js", "types/**/*", "__tests__/**/*", "tests/**/*"],
"exclude": ["node_modules", "../../packages/mobile-sdk-alpha/src"]
}

View File

@@ -7,14 +7,13 @@ import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitest/config';
const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, '../..');
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
include: ['__tests__/**/*.test.{ts,tsx}', 'tests/**/*.test.{ts,tsx}'],
include: ['tests/**/*.test.{ts,tsx}'],
exclude: ['node_modules/**'],
// Skip checking node_modules for faster testing
server: {
@@ -23,10 +22,22 @@ export default defineConfig({
},
},
},
esbuild: {
target: 'node18',
},
build: {
target: 'node18',
},
resolve: {
alias: {
'@selfxyz/common': resolve(repoRoot, 'common/dist/cjs/index.cjs'),
'@selfxyz/mobile-sdk-alpha': resolve(repoRoot, 'packages/mobile-sdk-alpha/src/index.ts'),
},
alias: [
{
find: '@selfxyz/mobile-sdk-alpha',
replacement: resolve(__dirname, './tests/mocks/mobile-sdk-alpha.ts'),
},
{
find: 'react-native',
replacement: resolve(__dirname, './tests/mocks/react-native.ts'),
},
],
},
});

350
yarn.lock
View File

@@ -7462,7 +7462,7 @@ __metadata:
eslint-plugin-react: "npm:^7.37.5"
eslint-plugin-simple-import-sort: "npm:^12.1.1"
eslint-plugin-sort-exports: "npm:^0.9.1"
jsdom: "npm:^24.0.0"
jsdom: "npm:^25.0.1"
node-forge: "npm:^1.3.1"
prettier: "npm:^3.5.3"
react: "npm:^18.3.1"
@@ -7474,7 +7474,7 @@ __metadata:
tsup: "npm:^8.0.1"
typescript: "npm:^5.9.2"
uuid: "npm:^11.1.0"
vitest: "npm:^1.6.0"
vitest: "npm:^2.1.8"
xstate: "npm:^5.20.2"
zustand: "npm:^4.5.2"
peerDependencies:
@@ -12674,17 +12674,6 @@ __metadata:
languageName: node
linkType: hard
"@vitest/expect@npm:1.6.1":
version: 1.6.1
resolution: "@vitest/expect@npm:1.6.1"
dependencies:
"@vitest/spy": "npm:1.6.1"
"@vitest/utils": "npm:1.6.1"
chai: "npm:^4.3.10"
checksum: 10c0/278164b2a32a7019b443444f21111c5e32e4cadee026cae047ae2a3b347d99dca1d1fb7b79509c88b67dc3db19fa9a16265b7d7a8377485f7e37f7851e44495a
languageName: node
linkType: hard
"@vitest/expect@npm:2.1.9":
version: 2.1.9
resolution: "@vitest/expect@npm:2.1.9"
@@ -12725,17 +12714,6 @@ __metadata:
languageName: node
linkType: hard
"@vitest/runner@npm:1.6.1":
version: 1.6.1
resolution: "@vitest/runner@npm:1.6.1"
dependencies:
"@vitest/utils": "npm:1.6.1"
p-limit: "npm:^5.0.0"
pathe: "npm:^1.1.1"
checksum: 10c0/36333f1a596c4ad85d42c6126cc32959c984d584ef28d366d366fa3672678c1a0f5e5c2e8717a36675b6620b57e8830f765d6712d1687f163ed0a8ebf23c87db
languageName: node
linkType: hard
"@vitest/runner@npm:2.1.9":
version: 2.1.9
resolution: "@vitest/runner@npm:2.1.9"
@@ -12746,17 +12724,6 @@ __metadata:
languageName: node
linkType: hard
"@vitest/snapshot@npm:1.6.1":
version: 1.6.1
resolution: "@vitest/snapshot@npm:1.6.1"
dependencies:
magic-string: "npm:^0.30.5"
pathe: "npm:^1.1.1"
pretty-format: "npm:^29.7.0"
checksum: 10c0/68bbc3132c195ec37376469e4b183fc408e0aeedd827dffcc899aac378e9ea324825f0873062786e18f00e3da9dd8a93c9bb871c07471ee483e8df963cb272eb
languageName: node
linkType: hard
"@vitest/snapshot@npm:2.1.9":
version: 2.1.9
resolution: "@vitest/snapshot@npm:2.1.9"
@@ -12768,15 +12735,6 @@ __metadata:
languageName: node
linkType: hard
"@vitest/spy@npm:1.6.1":
version: 1.6.1
resolution: "@vitest/spy@npm:1.6.1"
dependencies:
tinyspy: "npm:^2.2.0"
checksum: 10c0/5207ec0e7882819f0e0811293ae6d14163e26927e781bb4de7d40b3bd99c1fae656934c437bb7a30443a3e7e736c5bccb037bbf4436dbbc83d29e65247888885
languageName: node
linkType: hard
"@vitest/spy@npm:2.1.9":
version: 2.1.9
resolution: "@vitest/spy@npm:2.1.9"
@@ -12803,18 +12761,6 @@ __metadata:
languageName: node
linkType: hard
"@vitest/utils@npm:1.6.1":
version: 1.6.1
resolution: "@vitest/utils@npm:1.6.1"
dependencies:
diff-sequences: "npm:^29.6.3"
estree-walker: "npm:^3.0.3"
loupe: "npm:^2.3.7"
pretty-format: "npm:^29.7.0"
checksum: 10c0/0d4c619e5688cbc22a60c412719c6baa40376b7671bdbdc3072552f5c5a5ee5d24a96ea328b054018debd49e0626a5e3db672921b2c6b5b17b9a52edd296806a
languageName: node
linkType: hard
"@vitest/utils@npm:2.1.9":
version: 2.1.9
resolution: "@vitest/utils@npm:2.1.9"
@@ -13322,7 +13268,7 @@ __metadata:
languageName: node
linkType: hard
"acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.3.2":
"acorn-walk@npm:^8.1.1":
version: 8.3.4
resolution: "acorn-walk@npm:8.3.4"
dependencies:
@@ -14987,7 +14933,7 @@ __metadata:
languageName: node
linkType: hard
"chai@npm:^4.3.10, chai@npm:^4.3.6, chai@npm:^4.4.1":
"chai@npm:^4.3.6, chai@npm:^4.4.1":
version: 4.5.0
resolution: "chai@npm:4.5.0"
dependencies:
@@ -16246,7 +16192,7 @@ __metadata:
languageName: node
linkType: hard
"cssstyle@npm:^4.0.1, cssstyle@npm:^4.1.0":
"cssstyle@npm:^4.1.0":
version: 4.6.0
resolution: "cssstyle@npm:4.6.0"
dependencies:
@@ -18519,23 +18465,6 @@ __metadata:
languageName: node
linkType: hard
"execa@npm:^8.0.1":
version: 8.0.1
resolution: "execa@npm:8.0.1"
dependencies:
cross-spawn: "npm:^7.0.3"
get-stream: "npm:^8.0.1"
human-signals: "npm:^5.0.0"
is-stream: "npm:^3.0.0"
merge-stream: "npm:^2.0.0"
npm-run-path: "npm:^5.1.0"
onetime: "npm:^6.0.0"
signal-exit: "npm:^4.1.0"
strip-final-newline: "npm:^3.0.0"
checksum: 10c0/2c52d8775f5bf103ce8eec9c7ab3059909ba350a5164744e9947ed14a53f51687c040a250bda833f906d1283aa8803975b84e6c8f7a7c42f99dc8ef80250d1af
languageName: node
linkType: hard
"exit@npm:^0.1.2":
version: 0.1.2
resolution: "exit@npm:0.1.2"
@@ -19602,13 +19531,6 @@ __metadata:
languageName: node
linkType: hard
"get-stream@npm:^8.0.1":
version: 8.0.1
resolution: "get-stream@npm:8.0.1"
checksum: 10c0/5c2181e98202b9dae0bb4a849979291043e5892eb40312b47f0c22b9414fc9b28a3b6063d2375705eb24abc41ecf97894d9a51f64ff021511b504477b27b4290
languageName: node
linkType: hard
"get-symbol-description@npm:^1.1.0":
version: 1.1.0
resolution: "get-symbol-description@npm:1.1.0"
@@ -20443,13 +20365,6 @@ __metadata:
languageName: node
linkType: hard
"human-signals@npm:^5.0.0":
version: 5.0.0
resolution: "human-signals@npm:5.0.0"
checksum: 10c0/5a9359073fe17a8b58e5a085e9a39a950366d9f00217c4ff5878bd312e09d80f460536ea6a3f260b5943a01fe55c158d1cea3fc7bee3d0520aeef04f6d915c82
languageName: node
linkType: hard
"husky@npm:9.1.7":
version: 9.1.7
resolution: "husky@npm:9.1.7"
@@ -21167,13 +21082,6 @@ __metadata:
languageName: node
linkType: hard
"is-stream@npm:^3.0.0":
version: 3.0.0
resolution: "is-stream@npm:3.0.0"
checksum: 10c0/eb2f7127af02ee9aa2a0237b730e47ac2de0d4e76a4a905a50a11557f2339df5765eaea4ceb8029f1efa978586abe776908720bfcb1900c20c6ec5145f6f29d8
languageName: node
linkType: hard
"is-string@npm:^1.0.7, is-string@npm:^1.1.1":
version: 1.1.1
resolution: "is-string@npm:1.1.1"
@@ -22040,13 +21948,6 @@ __metadata:
languageName: node
linkType: hard
"js-tokens@npm:^9.0.1":
version: 9.0.1
resolution: "js-tokens@npm:9.0.1"
checksum: 10c0/68dcab8f233dde211a6b5fd98079783cbcd04b53617c1250e3553ee16ab3e6134f5e65478e41d82f6d351a052a63d71024553933808570f04dbf828d7921e80e
languageName: node
linkType: hard
"js-yaml@npm:3.x, js-yaml@npm:^3.10.0, js-yaml@npm:^3.13.1":
version: 3.14.1
resolution: "js-yaml@npm:3.14.1"
@@ -22122,40 +22023,6 @@ __metadata:
languageName: node
linkType: hard
"jsdom@npm:^24.0.0":
version: 24.1.3
resolution: "jsdom@npm:24.1.3"
dependencies:
cssstyle: "npm:^4.0.1"
data-urls: "npm:^5.0.0"
decimal.js: "npm:^10.4.3"
form-data: "npm:^4.0.0"
html-encoding-sniffer: "npm:^4.0.0"
http-proxy-agent: "npm:^7.0.2"
https-proxy-agent: "npm:^7.0.5"
is-potential-custom-element-name: "npm:^1.0.1"
nwsapi: "npm:^2.2.12"
parse5: "npm:^7.1.2"
rrweb-cssom: "npm:^0.7.1"
saxes: "npm:^6.0.0"
symbol-tree: "npm:^3.2.4"
tough-cookie: "npm:^4.1.4"
w3c-xmlserializer: "npm:^5.0.0"
webidl-conversions: "npm:^7.0.0"
whatwg-encoding: "npm:^3.1.1"
whatwg-mimetype: "npm:^4.0.0"
whatwg-url: "npm:^14.0.0"
ws: "npm:^8.18.0"
xml-name-validator: "npm:^5.0.0"
peerDependencies:
canvas: ^2.11.2
peerDependenciesMeta:
canvas:
optional: true
checksum: 10c0/e48b342afacd7418a23dac204a62deea729c50f4d072a7c04c09fd32355fdb4335f8779fa79fd0277a2dbeb2d356250a950955719d00047324b251233b11277f
languageName: node
linkType: hard
"jsdom@npm:^25.0.1":
version: 25.0.1
resolution: "jsdom@npm:25.0.1"
@@ -22764,16 +22631,6 @@ __metadata:
languageName: node
linkType: hard
"local-pkg@npm:^0.5.0":
version: 0.5.1
resolution: "local-pkg@npm:0.5.1"
dependencies:
mlly: "npm:^1.7.3"
pkg-types: "npm:^1.2.1"
checksum: 10c0/ade8346f1dc04875921461adee3c40774b00d4b74095261222ebd4d5fd0a444676e36e325f76760f21af6a60bc82480e154909b54d2d9f7173671e36dacf1808
languageName: node
linkType: hard
"localforage@npm:^1.10.0":
version: 1.10.0
resolution: "localforage@npm:1.10.0"
@@ -23009,7 +22866,7 @@ __metadata:
languageName: node
linkType: hard
"loupe@npm:^2.3.6, loupe@npm:^2.3.7":
"loupe@npm:^2.3.6":
version: 2.3.7
resolution: "loupe@npm:2.3.7"
dependencies:
@@ -23096,7 +22953,7 @@ __metadata:
languageName: node
linkType: hard
"magic-string@npm:^0.30.12, magic-string@npm:^0.30.17, magic-string@npm:^0.30.5":
"magic-string@npm:^0.30.12, magic-string@npm:^0.30.17":
version: 0.30.19
resolution: "magic-string@npm:0.30.19"
dependencies:
@@ -23681,13 +23538,6 @@ __metadata:
languageName: node
linkType: hard
"mimic-fn@npm:^4.0.0":
version: 4.0.0
resolution: "mimic-fn@npm:4.0.0"
checksum: 10c0/de9cc32be9996fd941e512248338e43407f63f6d497abe8441fa33447d922e927de54d4cc3c1a3c6d652857acd770389d5a3823f311a744132760ce2be15ccbf
languageName: node
linkType: hard
"mimic-function@npm:^5.0.0":
version: 5.0.1
resolution: "mimic-function@npm:5.0.1"
@@ -23940,7 +23790,7 @@ __metadata:
languageName: node
linkType: hard
"mlly@npm:^1.7.3, mlly@npm:^1.7.4":
"mlly@npm:^1.7.4":
version: 1.8.0
resolution: "mlly@npm:1.8.0"
dependencies:
@@ -24701,15 +24551,6 @@ __metadata:
languageName: node
linkType: hard
"npm-run-path@npm:^5.1.0":
version: 5.3.0
resolution: "npm-run-path@npm:5.3.0"
dependencies:
path-key: "npm:^4.0.0"
checksum: 10c0/124df74820c40c2eb9a8612a254ea1d557ddfab1581c3e751f825e3e366d9f00b0d76a3c94ecd8398e7f3eee193018622677e95816e8491f0797b21e30b2deba
languageName: node
linkType: hard
"nth-check@npm:^2.0.1, nth-check@npm:^2.1.1":
version: 2.1.1
resolution: "nth-check@npm:2.1.1"
@@ -24916,15 +24757,6 @@ __metadata:
languageName: node
linkType: hard
"onetime@npm:^6.0.0":
version: 6.0.0
resolution: "onetime@npm:6.0.0"
dependencies:
mimic-fn: "npm:^4.0.0"
checksum: 10c0/4eef7c6abfef697dd4479345a4100c382d73c149d2d56170a54a07418c50816937ad09500e1ed1e79d235989d073a9bade8557122aee24f0576ecde0f392bb6c
languageName: node
linkType: hard
"onetime@npm:^7.0.0":
version: 7.0.0
resolution: "onetime@npm:7.0.0"
@@ -25219,15 +25051,6 @@ __metadata:
languageName: node
linkType: hard
"p-limit@npm:^5.0.0":
version: 5.0.0
resolution: "p-limit@npm:5.0.0"
dependencies:
yocto-queue: "npm:^1.0.0"
checksum: 10c0/574e93b8895a26e8485eb1df7c4b58a1a6e8d8ae41b1750cc2cc440922b3d306044fc6e9a7f74578a883d46802d9db72b30f2e612690fcef838c173261b1ed83
languageName: node
linkType: hard
"p-locate@npm:^3.0.0":
version: 3.0.0
resolution: "p-locate@npm:3.0.0"
@@ -25512,13 +25335,6 @@ __metadata:
languageName: node
linkType: hard
"path-key@npm:^4.0.0":
version: 4.0.0
resolution: "path-key@npm:4.0.0"
checksum: 10c0/794efeef32863a65ac312f3c0b0a99f921f3e827ff63afa5cb09a377e202c262b671f7b3832a4e64731003fa94af0263713962d317b9887bd1e0c48a342efba3
languageName: node
linkType: hard
"path-parse@npm:^1.0.6, path-parse@npm:^1.0.7":
version: 1.0.7
resolution: "path-parse@npm:1.0.7"
@@ -25567,7 +25383,7 @@ __metadata:
languageName: node
linkType: hard
"pathe@npm:^1.1.1, pathe@npm:^1.1.2":
"pathe@npm:^1.1.2":
version: 1.1.2
resolution: "pathe@npm:1.1.2"
checksum: 10c0/64ee0a4e587fb0f208d9777a6c56e4f9050039268faaaaecd50e959ef01bf847b7872785c36483fa5cdcdbdfdb31fef2ff222684d4fc21c330ab60395c681897
@@ -25697,7 +25513,7 @@ __metadata:
languageName: node
linkType: hard
"pkg-types@npm:^1.2.1, pkg-types@npm:^1.3.1":
"pkg-types@npm:^1.3.1":
version: 1.3.1
resolution: "pkg-types@npm:1.3.1"
dependencies:
@@ -26143,7 +25959,7 @@ __metadata:
languageName: node
linkType: hard
"psl@npm:^1.1.33, psl@npm:^1.9.0":
"psl@npm:^1.9.0":
version: 1.15.0
resolution: "psl@npm:1.15.0"
dependencies:
@@ -26259,13 +26075,6 @@ __metadata:
languageName: node
linkType: hard
"querystringify@npm:^2.1.1":
version: 2.2.0
resolution: "querystringify@npm:2.2.0"
checksum: 10c0/3258bc3dbdf322ff2663619afe5947c7926a6ef5fb78ad7d384602974c467fadfc8272af44f5eb8cddd0d011aae8fabf3a929a8eee4b86edcc0a21e6bd10f9aa
languageName: node
linkType: hard
"queue-microtask@npm:^1.2.2":
version: 1.2.3
resolution: "queue-microtask@npm:1.2.3"
@@ -28886,7 +28695,7 @@ __metadata:
languageName: node
linkType: hard
"std-env@npm:^3.5.0, std-env@npm:^3.8.0":
"std-env@npm:^3.8.0":
version: 3.9.0
resolution: "std-env@npm:3.9.0"
checksum: 10c0/4a6f9218aef3f41046c3c7ecf1f98df00b30a07f4f35c6d47b28329bc2531eef820828951c7d7b39a1c5eb19ad8a46e3ddfc7deb28f0a2f3ceebee11bab7ba50
@@ -29206,13 +29015,6 @@ __metadata:
languageName: node
linkType: hard
"strip-final-newline@npm:^3.0.0":
version: 3.0.0
resolution: "strip-final-newline@npm:3.0.0"
checksum: 10c0/a771a17901427bac6293fd416db7577e2bc1c34a19d38351e9d5478c3c415f523f391003b42ed475f27e33a78233035df183525395f731d3bfb8cdcbd4da08ce
languageName: node
linkType: hard
"strip-hex-prefix@npm:1.0.0":
version: 1.0.0
resolution: "strip-hex-prefix@npm:1.0.0"
@@ -29245,15 +29047,6 @@ __metadata:
languageName: node
linkType: hard
"strip-literal@npm:^2.0.0":
version: 2.1.1
resolution: "strip-literal@npm:2.1.1"
dependencies:
js-tokens: "npm:^9.0.1"
checksum: 10c0/66a7353f5ba1ae6a4fb2805b4aba228171847200640083117c41512692e6b2c020e18580402984f55c0ae69c30f857f9a55abd672863e4ca8fdb463fdf93ba19
languageName: node
linkType: hard
"strnum@npm:^1.1.1":
version: 1.1.2
resolution: "strnum@npm:1.1.2"
@@ -29771,7 +29564,7 @@ __metadata:
languageName: node
linkType: hard
"tinybench@npm:^2.5.1, tinybench@npm:^2.9.0":
"tinybench@npm:^2.9.0":
version: 2.9.0
resolution: "tinybench@npm:2.9.0"
checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c
@@ -29812,13 +29605,6 @@ __metadata:
languageName: node
linkType: hard
"tinypool@npm:^0.8.3":
version: 0.8.4
resolution: "tinypool@npm:0.8.4"
checksum: 10c0/779c790adcb0316a45359652f4b025958c1dff5a82460fe49f553c864309b12ad732c8288be52f852973bc76317f5e7b3598878aee0beb8a33322c0e72c4a66c
languageName: node
linkType: hard
"tinypool@npm:^1.0.1":
version: 1.1.1
resolution: "tinypool@npm:1.1.1"
@@ -29833,13 +29619,6 @@ __metadata:
languageName: node
linkType: hard
"tinyspy@npm:^2.2.0":
version: 2.2.1
resolution: "tinyspy@npm:2.2.1"
checksum: 10c0/0b4cfd07c09871e12c592dfa7b91528124dc49a4766a0b23350638c62e6a483d5a2a667de7e6282246c0d4f09996482ddaacbd01f0c05b7ed7e0f79d32409bdc
languageName: node
linkType: hard
"tinyspy@npm:^3.0.2":
version: 3.0.2
resolution: "tinyspy@npm:3.0.2"
@@ -29931,18 +29710,6 @@ __metadata:
languageName: node
linkType: hard
"tough-cookie@npm:^4.1.4":
version: 4.1.4
resolution: "tough-cookie@npm:4.1.4"
dependencies:
psl: "npm:^1.1.33"
punycode: "npm:^2.1.1"
universalify: "npm:^0.2.0"
url-parse: "npm:^1.5.3"
checksum: 10c0/aca7ff96054f367d53d1e813e62ceb7dd2eda25d7752058a74d64b7266fd07be75908f3753a32ccf866a2f997604b414cfb1916d6e7f69bc64d9d9939b0d6c45
languageName: node
linkType: hard
"tough-cookie@npm:^5.0.0":
version: 5.1.2
resolution: "tough-cookie@npm:5.1.2"
@@ -30676,13 +30443,6 @@ __metadata:
languageName: node
linkType: hard
"universalify@npm:^0.2.0":
version: 0.2.0
resolution: "universalify@npm:0.2.0"
checksum: 10c0/cedbe4d4ca3967edf24c0800cfc161c5a15e240dac28e3ce575c689abc11f2c81ccc6532c8752af3b40f9120fb5e454abecd359e164f4f6aa44c29cd37e194fe
languageName: node
linkType: hard
"universalify@npm:^2.0.0":
version: 2.0.1
resolution: "universalify@npm:2.0.1"
@@ -30787,16 +30547,6 @@ __metadata:
languageName: node
linkType: hard
"url-parse@npm:^1.5.3":
version: 1.5.10
resolution: "url-parse@npm:1.5.10"
dependencies:
querystringify: "npm:^2.1.1"
requires-port: "npm:^1.0.0"
checksum: 10c0/bd5aa9389f896974beb851c112f63b466505a04b4807cea2e5a3b7092f6fbb75316f0491ea84e44f66fed55f1b440df5195d7e3a8203f64fcefa19d182f5be87
languageName: node
linkType: hard
"use-callback-ref@npm:^1.3.3":
version: 1.3.3
resolution: "use-callback-ref@npm:1.3.3"
@@ -30984,21 +30734,6 @@ __metadata:
languageName: node
linkType: hard
"vite-node@npm:1.6.1":
version: 1.6.1
resolution: "vite-node@npm:1.6.1"
dependencies:
cac: "npm:^6.7.14"
debug: "npm:^4.3.4"
pathe: "npm:^1.1.1"
picocolors: "npm:^1.0.0"
vite: "npm:^5.0.0"
bin:
vite-node: vite-node.mjs
checksum: 10c0/4d96da9f11bd0df8b60c46e65a740edaad7dd2d1aff3cdb3da5714ea8c10b5f2683111b60bfe45545c7e8c1f33e7e8a5095573d5e9ba55f50a845233292c2e02
languageName: node
linkType: hard
"vite-node@npm:2.1.9":
version: 2.1.9
resolution: "vite-node@npm:2.1.9"
@@ -31180,56 +30915,6 @@ __metadata:
languageName: node
linkType: hard
"vitest@npm:^1.6.0":
version: 1.6.1
resolution: "vitest@npm:1.6.1"
dependencies:
"@vitest/expect": "npm:1.6.1"
"@vitest/runner": "npm:1.6.1"
"@vitest/snapshot": "npm:1.6.1"
"@vitest/spy": "npm:1.6.1"
"@vitest/utils": "npm:1.6.1"
acorn-walk: "npm:^8.3.2"
chai: "npm:^4.3.10"
debug: "npm:^4.3.4"
execa: "npm:^8.0.1"
local-pkg: "npm:^0.5.0"
magic-string: "npm:^0.30.5"
pathe: "npm:^1.1.1"
picocolors: "npm:^1.0.0"
std-env: "npm:^3.5.0"
strip-literal: "npm:^2.0.0"
tinybench: "npm:^2.5.1"
tinypool: "npm:^0.8.3"
vite: "npm:^5.0.0"
vite-node: "npm:1.6.1"
why-is-node-running: "npm:^2.2.2"
peerDependencies:
"@edge-runtime/vm": "*"
"@types/node": ^18.0.0 || >=20.0.0
"@vitest/browser": 1.6.1
"@vitest/ui": 1.6.1
happy-dom: "*"
jsdom: "*"
peerDependenciesMeta:
"@edge-runtime/vm":
optional: true
"@types/node":
optional: true
"@vitest/browser":
optional: true
"@vitest/ui":
optional: true
happy-dom:
optional: true
jsdom:
optional: true
bin:
vitest: vitest.mjs
checksum: 10c0/511d27d7f697683964826db2fad7ac303f9bc7eeb59d9422111dc488371ccf1f9eed47ac3a80eb47ca86b7242228ba5ca9cc3613290830d0e916973768cac215
languageName: node
linkType: hard
"vitest@npm:^2.1.8":
version: 2.1.9
resolution: "vitest@npm:2.1.9"
@@ -31835,7 +31520,7 @@ __metadata:
languageName: node
linkType: hard
"why-is-node-running@npm:^2.2.2, why-is-node-running@npm:^2.3.0":
"why-is-node-running@npm:^2.3.0":
version: 2.3.0
resolution: "why-is-node-running@npm:2.3.0"
dependencies:
@@ -32253,13 +31938,6 @@ __metadata:
languageName: node
linkType: hard
"yocto-queue@npm:^1.0.0":
version: 1.2.1
resolution: "yocto-queue@npm:1.2.1"
checksum: 10c0/5762caa3d0b421f4bdb7a1926b2ae2189fc6e4a14469258f183600028eb16db3e9e0306f46e8ebf5a52ff4b81a881f22637afefbef5399d6ad440824e9b27f9f
languageName: node
linkType: hard
"yoctocolors-cjs@npm:^2.1.2":
version: 2.1.3
resolution: "yoctocolors-cjs@npm:2.1.3"