diff --git a/app/package.json b/app/package.json index 8a30dda4a..a4028ba3d 100644 --- a/app/package.json +++ b/app/package.json @@ -32,7 +32,7 @@ "install-app": "yarn install-app:setup && yarn clean:xcode-env-local", "install-app:mobile-deploy": "yarn install && yarn build:deps && yarn clean:xcode-env-local", "install-app:setup": "yarn install && yarn build:deps && yarn setup:android-deps && cd ios && bundle install && scripts/pod-install-with-cache-fix.sh && cd ..", - "ios": "yarn build:deps && react-native run-ios --scheme OpenPassport", + "ios": "yarn build:deps && node scripts/run-ios-simulator.cjs", "ios:fastlane-debug": "yarn reinstall && bundle exec fastlane --verbose ios internal_test", "lint": "eslint .", "lint:fix": "eslint --fix .", diff --git a/app/scripts/run-ios-simulator.cjs b/app/scripts/run-ios-simulator.cjs new file mode 100644 index 000000000..87028003f --- /dev/null +++ b/app/scripts/run-ios-simulator.cjs @@ -0,0 +1,52 @@ +// 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. + +const { execSync } = require('child_process'); + +try { + // Get list of available simulators + const output = execSync('xcrun simctl list devices available --json', { + encoding: 'utf8', + }); + + const devices = JSON.parse(output).devices; + + // Find first available iPhone simulator (prefer latest iOS version) + let firstSimulator = null; + + // Get iOS runtime keys sorted in reverse (latest first) + const runtimeKeys = Object.keys(devices) + .filter(key => key.includes('iOS')) + .sort() + .reverse(); + + for (const runtime of runtimeKeys) { + const iPhones = devices[runtime].filter( + device => device.name.startsWith('iPhone') && device.isAvailable, + ); + + if (iPhones.length > 0) { + firstSimulator = iPhones[0].name; + break; + } + } + + if (!firstSimulator) { + console.error('No available iPhone simulators found'); + process.exit(1); + } + + console.log(`Using simulator: ${firstSimulator}`); + + // Run the iOS build with the selected simulator + execSync( + `react-native run-ios --scheme OpenPassport --simulator="${firstSimulator}"`, + { + stdio: 'inherit', + }, + ); +} catch (error) { + console.error('Failed to run iOS simulator:', error.message); + process.exit(1); +} diff --git a/app/src/screens/prove/ProofRequestStatusScreen.tsx b/app/src/screens/prove/ProofRequestStatusScreen.tsx index e8f12ee53..5258398ae 100644 --- a/app/src/screens/prove/ProofRequestStatusScreen.tsx +++ b/app/src/screens/prove/ProofRequestStatusScreen.tsx @@ -96,8 +96,7 @@ const SuccessScreen: React.FC = () => { } } catch { console.warn( - 'Invalid deep link URL provided:', - selfApp.deeplinkCallback, + 'Invalid deep link URL provided (URL sanitized for security)', ); } } diff --git a/app/version.json b/app/version.json index d2d746e14..9a3992cfd 100644 --- a/app/version.json +++ b/app/version.json @@ -1,6 +1,6 @@ { "ios": { - "build": 175, + "build": 176, "lastDeployed": "2025-09-30T16:35:10Z" }, "android": { diff --git a/common/index.ts b/common/index.ts index efb066495..39f3387d6 100644 --- a/common/index.ts +++ b/common/index.ts @@ -18,12 +18,12 @@ export type { UserIdType, } from './src/utils/index.js'; -// Additional type exports -export type { Environment } from './src/utils/types.js'; - // Constants exports export type { Country3LetterCode } from './src/constants/index.js'; +// Additional type exports +export type { Environment } from './src/utils/types.js'; + // Utils exports export { API_URL, @@ -73,6 +73,7 @@ export { genAndInitMockPassportData, genMockIdDoc, genMockIdDocAndInitDataParsing, + fetchOfacTrees, generateCircuitInputsDSC, generateCircuitInputsRegister, generateCircuitInputsRegisterForTests, @@ -80,12 +81,11 @@ export { generateCommitment, generateMockDSC, generateNullifier, + generateTEEInputsDiscloseStateless, getCircuitNameFromPassportData, getLeafCscaTree, getLeafDscTree, - fetchOfacTrees, getSKIPEM, - generateTEEInputsDiscloseStateless, getSolidityPackedUserContextData, getUniversalLink, hashEndpointWithScope, diff --git a/common/src/constants/constants.ts b/common/src/constants/constants.ts index e93723b00..4e769c0d2 100644 --- a/common/src/constants/constants.ts +++ b/common/src/constants/constants.ts @@ -136,7 +136,7 @@ export const MAX_PADDED_ECONTENT_LEN: Partial = { +export const MAX_PADDED_SIGNED_ATTR_LEN: Record<(typeof hashAlgos)[number], number> = { sha1: 128, sha224: 128, sha256: 256, @@ -144,7 +144,7 @@ export const MAX_PADDED_SIGNED_ATTR_LEN_FOR_TESTS: Record<(typeof hashAlgos)[num sha512: 256, }; -export const MAX_PADDED_SIGNED_ATTR_LEN: Record<(typeof hashAlgos)[number], number> = { +export const MAX_PADDED_SIGNED_ATTR_LEN_FOR_TESTS: Record<(typeof hashAlgos)[number], number> = { sha1: 128, sha224: 128, sha256: 256, diff --git a/common/src/polyfills/crypto.ts b/common/src/polyfills/crypto.ts index 00bd8429c..2883713c4 100644 --- a/common/src/polyfills/crypto.ts +++ b/common/src/polyfills/crypto.ts @@ -94,7 +94,12 @@ function createHmac(algorithm: string, key: string | Uint8Array) { throw new Error(`Unsupported HMAC algorithm: ${algorithm}`); } - const keyBytes = typeof key === 'string' ? new TextEncoder().encode(key) : key; + const keyBytes = + typeof key === 'string' + ? new TextEncoder().encode(key) + : ArrayBuffer.isView(key) && !(key instanceof Uint8Array && key.constructor === Uint8Array) + ? new Uint8Array(key.buffer, key.byteOffset, key.byteLength) + : key; const hmacState = hmac.create(hashFn, keyBytes); let finalized = false; diff --git a/common/src/utils/aadhaar/mockData.ts b/common/src/utils/aadhaar/mockData.ts index 78850d9b6..4cb80b69d 100644 --- a/common/src/utils/aadhaar/mockData.ts +++ b/common/src/utils/aadhaar/mockData.ts @@ -49,12 +49,15 @@ function computeUppercasePaddedName(name: string): number[] { .map((char) => char.charCodeAt(0)); } -export function convertByteArrayToBigInt(byteArray: Uint8Array | number[]): bigint { - let result = 0n; - for (let i = 0; i < byteArray.length; i++) { - result = result * 256n + BigInt(byteArray[i]); - } - return result; +// Helper function to compute final commitment +export function computeCommitment( + secret: bigint, + qrHash: bigint, + nullifier: bigint, + packedCommitment: bigint, + photoHash: bigint +): bigint { + return poseidon5([secret, qrHash, nullifier, packedCommitment, photoHash]); } // Helper function to compute packed commitment @@ -71,15 +74,12 @@ export function computePackedCommitment( return BigInt(packBytesAndPoseidon(packedCommitmentArgs)); } -// Helper function to compute final commitment -export function computeCommitment( - secret: bigint, - qrHash: bigint, - nullifier: bigint, - packedCommitment: bigint, - photoHash: bigint -): bigint { - return poseidon5([secret, qrHash, nullifier, packedCommitment, photoHash]); +export function convertByteArrayToBigInt(byteArray: Uint8Array | number[]): bigint { + let result = 0n; + for (let i = 0; i < byteArray.length; i++) { + result = result * 256n + BigInt(byteArray[i]); + } + return result; } interface SharedQRData { @@ -108,43 +108,6 @@ export function nullifierHash(extractedFields: ReturnType ({ ...acc, ...curr }), {}); } -export function generateCircuitInputsRegister( +export function generateCircuitInputsRegisterForTests( secret: string, passportData: PassportData, serializedDscTree: string @@ -302,7 +302,7 @@ export function generateCircuitInputsRegister( ); const [signedAttrPadded, signedAttrPaddedLen] = pad(passportMetadata.signedAttrHashFunction)( signedAttr, - MAX_PADDED_SIGNED_ATTR_LEN[passportMetadata.eContentHashFunction] + MAX_PADDED_SIGNED_ATTR_LEN_FOR_TESTS[passportMetadata.eContentHashFunction] ); const dsc_leaf = getLeafDscTree(dscParsed, passportData.csca_parsed); // TODO: WRONG diff --git a/common/src/utils/index.ts b/common/src/utils/index.ts index c7fb6220e..5f899a58d 100644 --- a/common/src/utils/index.ts +++ b/common/src/utils/index.ts @@ -48,8 +48,8 @@ export { getWSDbRelayerUrl, } from './proving.js'; export { extractQRDataFields, getAadharRegistrationWindow } from './aadhaar/utils.js'; -export { formatMrz } from './passports/format.js'; export { fetchOfacTrees } from './ofac.js'; +export { formatMrz } from './passports/format.js'; export { genAndInitMockPassportData } from './passports/genMockPassportData.js'; export { genMockIdDoc, diff --git a/common/src/utils/passports/genMockIdDoc.ts b/common/src/utils/passports/genMockIdDoc.ts index 0717da4a2..0b6e5f54c 100644 --- a/common/src/utils/passports/genMockIdDoc.ts +++ b/common/src/utils/passports/genMockIdDoc.ts @@ -7,6 +7,10 @@ import forge from 'node-forge'; import type { hashAlgosTypes } from '../../constants/constants.js'; import { API_URL_STAGING } from '../../constants/constants.js'; import { countries } from '../../constants/countries.js'; +import { + AADHAAR_MOCK_PRIVATE_KEY_PEM, + AADHAAR_MOCK_PUBLIC_KEY_PEM, +} from '../../mock_certificates/aadhaar/mockAadhaarCert.js'; import { convertByteArrayToBigInt, processQRData } from '../aadhaar/mockData.js'; import { extractQRDataFields } from '../aadhaar/utils.js'; import { getCurveForElliptic } from '../certificate_parsing/curves.js'; @@ -21,10 +25,6 @@ import { genDG1 } from './dg1.js'; import { formatAndConcatenateDataHashes, formatMrz, generateSignedAttr } from './format.js'; import { getMockDSC } from './getMockDSC.js'; import { initPassportDataParsing } from './passport.js'; -import { - AADHAAR_MOCK_PRIVATE_KEY_PEM, - AADHAAR_MOCK_PUBLIC_KEY_PEM, -} from '../../mock_certificates/aadhaar/mockAadhaarCert.js'; export interface IdDocInput { idType: 'mock_passport' | 'mock_id_card' | 'mock_aadhaar'; diff --git a/common/tests/cryptoHash.test.ts b/common/tests/cryptoHash.test.ts index 996b4d1c9..148d22a65 100644 --- a/common/tests/cryptoHash.test.ts +++ b/common/tests/cryptoHash.test.ts @@ -3,6 +3,7 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. import { createHash } from '../src/polyfills/crypto'; + import { sha256 } from '@noble/hashes/sha256'; describe('Hash Finalization', () => { diff --git a/common/tests/cryptoHmac.test.ts b/common/tests/cryptoHmac.test.ts index 200764927..49be5c13b 100644 --- a/common/tests/cryptoHmac.test.ts +++ b/common/tests/cryptoHmac.test.ts @@ -3,6 +3,7 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. import { createHmac } from '../src/polyfills/crypto'; + import { hmac } from '@noble/hashes/hmac'; import { sha256 } from '@noble/hashes/sha256'; diff --git a/packages/mobile-sdk-alpha/src/stores/protocolStore.ts b/packages/mobile-sdk-alpha/src/stores/protocolStore.ts index 162449084..d90cba2f3 100644 --- a/packages/mobile-sdk-alpha/src/stores/protocolStore.ts +++ b/packages/mobile-sdk-alpha/src/stores/protocolStore.ts @@ -27,6 +27,33 @@ import { import type { SelfClient } from '../types/public'; +/** + * Fetch with timeout helper + * @param url - URL to fetch + * @param options - Fetch options + * @param timeoutMs - Timeout in milliseconds (default: 30000) + * @returns Promise + */ +async function fetchWithTimeout(url: string, options?: RequestInit, timeoutMs: number = 30000): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetch(url, { + ...options, + signal: controller.signal, + }); + clearTimeout(timeoutId); + return response; + } catch (error) { + clearTimeout(timeoutId); + if (error instanceof Error && error.name === 'AbortError') { + throw new Error(`Request timeout after ${timeoutMs}ms`); + } + throw error; + } +} + export interface ProtocolState { passport: { commitment_tree: any; @@ -121,7 +148,7 @@ export const useProtocolStore = create((set, get) => ({ ]); }, fetch_alternative_csca: async (environment: 'prod' | 'stg', ski: string) => { - const url = `${environment === 'prod' ? API_URL : API_URL_STAGING}/ski-pems/${ski.toLowerCase()}`; // TODO: remove false once we have the endpoint in production + const url = `${environment === 'prod' ? API_URL : API_URL_STAGING}/ski-pems/${ski.toLowerCase()}`; try { const response = await fetch(url, { method: 'GET', @@ -140,7 +167,7 @@ export const useProtocolStore = create((set, get) => ({ fetch_deployed_circuits: async (environment: 'prod' | 'stg') => { const url = `${environment === 'prod' ? API_URL : API_URL_STAGING}/deployed-circuits`; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP error fetching ${url}! status: ${response.status}`); } @@ -149,12 +176,13 @@ export const useProtocolStore = create((set, get) => ({ set({ passport: { ...get().passport, deployed_circuits: data.data } }); } catch (error) { console.error(`Failed fetching deployed circuits from ${url}:`, error); + set({ passport: { ...get().passport, deployed_circuits: null } }); } }, fetch_circuits_dns_mapping: async (environment: 'prod' | 'stg') => { const url = `${environment === 'prod' ? API_URL : API_URL_STAGING}/circuit-dns-mapping-gcp`; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP error fetching ${url}! status: ${response.status}`); } @@ -165,12 +193,13 @@ export const useProtocolStore = create((set, get) => ({ }); } catch (error) { console.error(`Failed fetching circuit DNS mapping from ${url}:`, error); + set({ passport: { ...get().passport, circuits_dns_mapping: null } }); } }, fetch_csca_tree: async (environment: 'prod' | 'stg') => { const url = environment === 'prod' ? CSCA_TREE_URL : CSCA_TREE_URL_STAGING; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP error fetching ${url}! status: ${response.status}`); } @@ -192,7 +221,7 @@ export const useProtocolStore = create((set, get) => ({ fetch_dsc_tree: async (environment: 'prod' | 'stg') => { const url = environment === 'prod' ? DSC_TREE_URL : DSC_TREE_URL_STAGING; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP error fetching ${url}! status: ${response.status}`); } @@ -201,13 +230,13 @@ export const useProtocolStore = create((set, get) => ({ set({ passport: { ...get().passport, dsc_tree: data.data } }); } catch (error) { console.error(`Failed fetching DSC tree from ${url}:`, error); - // Optionally handle error state + set({ passport: { ...get().passport, dsc_tree: null } }); } }, fetch_identity_tree: async (environment: 'prod' | 'stg') => { const url = environment === 'prod' ? IDENTITY_TREE_URL : IDENTITY_TREE_URL_STAGING; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP error fetching ${url}! status: ${response.status}`); } @@ -216,6 +245,7 @@ export const useProtocolStore = create((set, get) => ({ set({ passport: { ...get().passport, commitment_tree: data.data } }); } catch (error) { console.error(`Failed fetching identity tree from ${url}:`, error); + set({ passport: { ...get().passport, commitment_tree: null } }); } }, fetch_ofac_trees: async (environment: 'prod' | 'stg') => { @@ -250,7 +280,7 @@ export const useProtocolStore = create((set, get) => ({ fetch_deployed_circuits: async (environment: 'prod' | 'stg') => { const url = `${environment === 'prod' ? API_URL : API_URL_STAGING}/deployed-circuits`; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP error fetching ${url}! status: ${response.status}`); } @@ -259,13 +289,13 @@ export const useProtocolStore = create((set, get) => ({ set({ id_card: { ...get().id_card, deployed_circuits: data.data } }); } catch (error) { console.error(`Failed fetching deployed circuits from ${url}:`, error); - // Optionally handle error state + set({ id_card: { ...get().id_card, deployed_circuits: null } }); } }, fetch_circuits_dns_mapping: async (environment: 'prod' | 'stg') => { const url = `${environment === 'prod' ? API_URL : API_URL_STAGING}/circuit-dns-mapping-gcp`; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP error fetching ${url}! status: ${response.status}`); } @@ -276,13 +306,13 @@ export const useProtocolStore = create((set, get) => ({ }); } catch (error) { console.error(`Failed fetching circuit DNS mapping from ${url}:`, error); - // Optionally handle error state + set({ id_card: { ...get().id_card, circuits_dns_mapping: null } }); } }, fetch_csca_tree: async (environment: 'prod' | 'stg') => { const url = environment === 'prod' ? CSCA_TREE_URL_ID_CARD : CSCA_TREE_URL_STAGING_ID_CARD; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP error fetching ${url}! status: ${response.status}`); } @@ -304,7 +334,7 @@ export const useProtocolStore = create((set, get) => ({ fetch_dsc_tree: async (environment: 'prod' | 'stg') => { const url = environment === 'prod' ? DSC_TREE_URL_ID_CARD : DSC_TREE_URL_STAGING_ID_CARD; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP error fetching ${url}! status: ${response.status}`); } @@ -313,13 +343,13 @@ export const useProtocolStore = create((set, get) => ({ set({ id_card: { ...get().id_card, dsc_tree: data.data } }); } catch (error) { console.error(`Failed fetching DSC tree from ${url}:`, error); - // Optionally handle error state + set({ id_card: { ...get().id_card, dsc_tree: null } }); } }, fetch_identity_tree: async (environment: 'prod' | 'stg') => { const url = environment === 'prod' ? IDENTITY_TREE_URL_ID_CARD : IDENTITY_TREE_URL_STAGING_ID_CARD; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP error fetching ${url}! status: ${response.status}`); } @@ -328,13 +358,13 @@ export const useProtocolStore = create((set, get) => ({ set({ id_card: { ...get().id_card, commitment_tree: data.data } }); } catch (error) { console.error(`Failed fetching identity tree from ${url}:`, error); - // Optionally handle error state + set({ id_card: { ...get().id_card, commitment_tree: null } }); } }, fetch_alternative_csca: async (environment: 'prod' | 'stg', ski: string) => { - const url = `${environment === 'prod' ? API_URL : API_URL_STAGING}/ski-pems/${ski.toLowerCase()}`; // TODO: remove false once we have the endpoint in production + const url = `${environment === 'prod' ? API_URL : API_URL_STAGING}/ski-pems/${ski.toLowerCase()}`; try { - const response = await fetch(url, { + const response = await fetchWithTimeout(url, { method: 'GET', }); if (!response.ok) { diff --git a/packages/mobile-sdk-demo/metro.config.cjs b/packages/mobile-sdk-demo/metro.config.cjs index 76e4d26b1..8c96308c2 100644 --- a/packages/mobile-sdk-demo/metro.config.cjs +++ b/packages/mobile-sdk-demo/metro.config.cjs @@ -86,25 +86,73 @@ const config = { // Fix @noble/hashes subpath export resolution if (moduleName.startsWith('@noble/hashes/')) { try { - // Extract the subpath (e.g., 'crypto.js', 'sha256', 'hmac') - const subpath = moduleName.replace('@noble/hashes/', ''); + // Extract the subpath (e.g., 'crypto.js', 'sha256', 'hmac', 'lib/sha256.js') + let subpath = moduleName.replace('@noble/hashes/', ''); + + // Find the package root directory const basePath = require.resolve('@noble/hashes'); + let packageRoot = path.dirname(basePath); + + // Traverse up to find package.json to get the real package root + while (packageRoot !== path.dirname(packageRoot)) { + const packageJsonPath = path.join(packageRoot, 'package.json'); + const fs = require('fs'); + if (fs.existsSync(packageJsonPath)) { + try { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + if (packageJson.name === '@noble/hashes') { + break; + } + } catch { + // Continue searching + } + } + packageRoot = path.dirname(packageRoot); + } + + // Normalize the subpath - try multiple locations + const candidatePaths = []; - // For .js files, look in the package directory if (subpath.endsWith('.js')) { - const subpathFile = path.join(path.dirname(basePath), subpath); - return { - type: 'sourceFile', - filePath: subpathFile, - }; + // Try the path as-is + candidatePaths.push(path.join(packageRoot, subpath)); + // If subpath contains 'lib/', also try without 'lib/' + if (subpath.startsWith('lib/')) { + candidatePaths.push(path.join(packageRoot, subpath.replace('lib/', ''))); + } } else { - // For other imports like 'sha256', 'hmac', etc., try the main directory - const subpathFile = path.join(path.dirname(basePath), `${subpath}.js`); - return { - type: 'sourceFile', - filePath: subpathFile, - }; + // For imports without .js extension + candidatePaths.push(path.join(packageRoot, `${subpath}.js`)); + candidatePaths.push(path.join(packageRoot, subpath, 'index.js')); + // Also try in lib directory + candidatePaths.push(path.join(packageRoot, 'lib', `${subpath}.js`)); } + + // Guard against path traversal: normalize and ensure within packageRoot + const fs = require('fs'); + const normalizedCandidates = candidatePaths + .map(p => path.resolve(p)) + .filter(p => { + const relative = path.relative(packageRoot, p); + // keep only files strictly inside packageRoot + return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative); + }); + + // Find the first existing file among safe candidates + for (const candidatePath of normalizedCandidates) { + if (fs.existsSync(candidatePath)) { + return { + type: 'sourceFile', + filePath: candidatePath, + }; + } + } + + // Fallback to main package if no candidate exists + return { + type: 'sourceFile', + filePath: require.resolve('@noble/hashes'), + }; } catch { // Fallback to main package if subpath doesn't exist return { diff --git a/packages/mobile-sdk-demo/package.json b/packages/mobile-sdk-demo/package.json index 28285d3cb..a0e603ad2 100644 --- a/packages/mobile-sdk-demo/package.json +++ b/packages/mobile-sdk-demo/package.json @@ -54,6 +54,7 @@ "@react-native-community/cli": "^16.0.3", "@react-native/metro-config": "0.76.9", "@tsconfig/react-native": "^3.0.6", + "@types/node": "^22.18.3", "@types/react": "^18.3.4", "@types/react-native-vector-icons": "^6.4.18", "@typescript-eslint/eslint-plugin": "^8.44.0", diff --git a/yarn.lock b/yarn.lock index 1f076c47e..2e9a89710 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23831,6 +23831,7 @@ __metadata: "@selfxyz/common": "workspace:*" "@selfxyz/mobile-sdk-alpha": "workspace:*" "@tsconfig/react-native": "npm:^3.0.6" + "@types/node": "npm:^22.18.3" "@types/react": "npm:^18.3.4" "@types/react-native-vector-icons": "npm:^6.4.18" "@typescript-eslint/eslint-plugin": "npm:^8.44.0"