From 430a6fd9735e2bb1119c1d01fc7ff2e1c8263b22 Mon Sep 17 00:00:00 2001 From: ayman Date: Thu, 11 Sep 2025 21:28:13 +0530 Subject: [PATCH 01/25] feat: add aadhaar support to the ts sdk --- sdk/core/src/SelfBackendVerifier.ts | 38 +++++++++----- sdk/core/src/utils/constants.ts | 33 ++++++++++++ sdk/core/src/utils/id.ts | 81 ++++++++++++++++++++++------- sdk/core/src/utils/proof.ts | 3 ++ 4 files changed, 122 insertions(+), 33 deletions(-) diff --git a/sdk/core/src/SelfBackendVerifier.ts b/sdk/core/src/SelfBackendVerifier.ts index 8f679200c..b898184d8 100644 --- a/sdk/core/src/SelfBackendVerifier.ts +++ b/sdk/core/src/SelfBackendVerifier.ts @@ -217,20 +217,30 @@ export class SelfBackendVerifier { }); } - const circuitTimestampYy = [ - 2, - 0, - publicSignals[discloseIndices[attestationId].currentDateIndex], - publicSignals[discloseIndices[attestationId].currentDateIndex + 1], - ]; - const circuitTimestampMm = [ - publicSignals[discloseIndices[attestationId].currentDateIndex + 2], - publicSignals[discloseIndices[attestationId].currentDateIndex + 3], - ]; - const circuitTimestampDd = [ - publicSignals[discloseIndices[attestationId].currentDateIndex + 4], - publicSignals[discloseIndices[attestationId].currentDateIndex + 5], - ]; + let circuitTimestampYy: number[]; + let circuitTimestampMm: number[]; + let circuitTimestampDd: number[]; + if (attestationId === 3) { + circuitTimestampYy = String(publicSignals[discloseIndices[attestationId].currentDateIndex]).split('').map(Number); + circuitTimestampMm = String(publicSignals[discloseIndices[attestationId].currentDateIndex + 1]).split('').map(Number); + circuitTimestampDd = String(publicSignals[discloseIndices[attestationId].currentDateIndex + 2]).split('').map(Number); + } else { + circuitTimestampYy = [ + 2, + 0, + +publicSignals[discloseIndices[attestationId].currentDateIndex], + +publicSignals[discloseIndices[attestationId].currentDateIndex + 1], + ]; + circuitTimestampMm = [ + +publicSignals[discloseIndices[attestationId].currentDateIndex + 2], + +publicSignals[discloseIndices[attestationId].currentDateIndex + 3], + ]; + circuitTimestampDd = [ + +publicSignals[discloseIndices[attestationId].currentDateIndex + 4], + +publicSignals[discloseIndices[attestationId].currentDateIndex + 5], + ]; + } + const circuitTimestamp = new Date( Number(circuitTimestampYy.join('')), Number(circuitTimestampMm.join('')) - 1, diff --git a/sdk/core/src/utils/constants.ts b/sdk/core/src/utils/constants.ts index 81992b070..cce86f397 100644 --- a/sdk/core/src/utils/constants.ts +++ b/sdk/core/src/utils/constants.ts @@ -27,6 +27,19 @@ export const discloseIndices = { userIdentifierIndex: 20, passportNoSmtRootIndex: 99, }, + 3: { + revealedDataPackedIndex: 2, + forbiddenCountriesListPackedIndex: 6, + nullifierIndex: 0, + attestationIdIndex: 10, + merkleRootIndex: 16, + currentDateIndex: 11, + namedobSmtRootIndex: 14, + nameyobSmtRootIndex: 15, + scopeIndex: 17, + userIdentifierIndex: 18, + passportNoSmtRootIndex: 99, + } } as const; type RevealedDataFields = @@ -84,6 +97,26 @@ export const revealedDataIndices: Record< ofacStart: 92, ofacEnd: 93, }, + 3: { + issuingStateStart: 81, + issuingStateEnd: 111, + nameStart: 9, + nameEnd: 70, + idNumberStart: 71, + idNumberEnd: 74, + nationalityStart: 999, + nationalityEnd: 999, + dateOfBirthStart: 1, + dateOfBirthEnd: 8, + genderStart: 0, + genderEnd: 0, + expiryDateStart: 999, + expiryDateEnd: 999, + olderThanStart: 118, + olderThanEnd: 118, + ofacStart: 116, + ofacEnd: 117, + }, } as const; const allIdEntries = Object.keys(discloseIndices).map( diff --git a/sdk/core/src/utils/id.ts b/sdk/core/src/utils/id.ts index 3097dfb37..61fb5adea 100644 --- a/sdk/core/src/utils/id.ts +++ b/sdk/core/src/utils/id.ts @@ -2,6 +2,15 @@ import { discloseIndices, revealedDataIndices } from './constants.js'; import { AttestationId, GenericDiscloseOutput } from 'src/types/types.js'; import { getRevealedDataBytes } from './proof.js'; +/** + * Removes null bytes (\x00) from a string + * @param str - The string to clean + * @returns The string with null bytes removed + */ +export const removeNullBytes = (str: string): string => { + return str.replace(/\x00/g, ''); +}; + export const formatRevealedDataPacked = ( attestationId: AttestationId, publicSignals: string[] @@ -12,7 +21,7 @@ export const formatRevealedDataPacked = ( const nullifier = publicSignals[discloseIndices[attestationId].nullifierIndex]; const forbiddenCountriesListPacked = publicSignals.slice( discloseIndices[attestationId].forbiddenCountriesListPackedIndex, - discloseIndices[attestationId].forbiddenCountriesListPackedIndex + 3 + discloseIndices[attestationId].forbiddenCountriesListPackedIndex + 4 ); const issuingState = revealedDataPackedString .subarray( @@ -35,48 +44,82 @@ export const formatRevealedDataPacked = ( revealedDataIndices[attestationId].idNumberEnd + 1 ) .toString('utf-8'); - const nationality = revealedDataPackedString + + let nationality: string; + if (attestationId === 3) { + nationality = 'IND'; + } else { + nationality = revealedDataPackedString .subarray( revealedDataIndices[attestationId].nationalityStart, revealedDataIndices[attestationId].nationalityEnd + 1 ) .toString('utf-8'); - const dateOfBirth = revealedDataPackedString - .subarray( + } + let dateOfBirth: string; + if (attestationId === 3) { + dateOfBirth = new Array(revealedDataPackedString.subarray( revealedDataIndices[attestationId].dateOfBirthStart, revealedDataIndices[attestationId].dateOfBirthEnd + 1 - ) - .toString('utf-8'); + )).map(Number).map(String).join(''); + } else { + dateOfBirth = revealedDataPackedString + .subarray( + revealedDataIndices[attestationId].dateOfBirthStart, + revealedDataIndices[attestationId].dateOfBirthEnd + 1 + ) + .toString('utf-8'); + } const gender = revealedDataPackedString .subarray( revealedDataIndices[attestationId].genderStart, revealedDataIndices[attestationId].genderEnd + 1 ) .toString('utf-8'); - const expiryDate = revealedDataPackedString - .subarray( - revealedDataIndices[attestationId].expiryDateStart, - revealedDataIndices[attestationId].expiryDateEnd + 1 - ) - .toString('utf-8'); - const olderThan = Buffer.from( - revealedDataPackedString.subarray( + let expiryDate: string; + if (attestationId === 3) { + expiryDate = 'UNAVAILABLE'; + } else { + expiryDate = revealedDataPackedString + .subarray( + revealedDataIndices[attestationId].expiryDateStart, + revealedDataIndices[attestationId].expiryDateEnd + 1 + ) + .toString('utf-8'); + } + let olderThan: string; + if (attestationId === 3) { + olderThan = revealedDataPackedString.subarray( revealedDataIndices[attestationId].olderThanStart, revealedDataIndices[attestationId].olderThanEnd + 1 - ) - ).toString('utf-8'); + ).toString('utf-8'); + } else { + olderThan = + revealedDataPackedString.subarray( + revealedDataIndices[attestationId].olderThanStart, + revealedDataIndices[attestationId].olderThanEnd + 1 + )[0].toString().padStart(2, '0'); + } + console.log('ofac', revealedDataPackedString.subarray( + revealedDataIndices[attestationId].ofacStart, + revealedDataIndices[attestationId].ofacEnd + 1 + )); const ofac = Array.from( revealedDataPackedString.subarray( revealedDataIndices[attestationId].ofacStart, revealedDataIndices[attestationId].ofacEnd + 1 ) - ).map(Boolean); + ).map(Boolean).map((x) => !x); + + if (ofac.length < 3) { + ofac.unshift(false); + } return { nullifier: nullifier.toString(), forbiddenCountriesListPacked: forbiddenCountriesListPacked, - issuingState: issuingState, - name: name, + issuingState: removeNullBytes(issuingState), + name: removeNullBytes(name), idNumber: idNumber, nationality: nationality, dateOfBirth: dateOfBirth, diff --git a/sdk/core/src/utils/proof.ts b/sdk/core/src/utils/proof.ts index d9e3640a8..4d7bcd4ae 100644 --- a/sdk/core/src/utils/proof.ts +++ b/sdk/core/src/utils/proof.ts @@ -17,6 +17,8 @@ export function getRevealedDataPublicSignalsLength(attestationId: AttestationId) return 93 / 31; case 2: return Math.ceil(94 / 31); + case 3: + return Math.ceil(119 / 31); default: throw new ProofError(`Invalid attestation ID: ${attestationId}`); } @@ -25,6 +27,7 @@ export function getRevealedDataPublicSignalsLength(attestationId: AttestationId) export const bytesCount: Record = { 1: [31, 31, 31], 2: [31, 31, 31, 1], + 3: [31, 31, 31, 26] }; /** From 08ccf793629500cacf00f1d9650bb384bf13e74b Mon Sep 17 00:00:00 2001 From: vishal Date: Fri, 12 Sep 2025 11:27:37 +0530 Subject: [PATCH 02/25] feat: aadhaar support to go sdk --- sdk/sdk-go/utils.go | 88 ++++++++++++++++++++++++++++++++++++++---- sdk/sdk-go/verifier.go | 76 ++++++++++++++++++++++++++++-------- 2 files changed, 140 insertions(+), 24 deletions(-) diff --git a/sdk/sdk-go/utils.go b/sdk/sdk-go/utils.go index d236886b4..5e673391c 100644 --- a/sdk/sdk-go/utils.go +++ b/sdk/sdk-go/utils.go @@ -16,6 +16,7 @@ import ( const ( Passport AttestationId = 1 EUCard AttestationId = 2 + Aadhaar AttestationId = 3 ) // DiscloseIndicesEntry defines the indices for different data fields in the public signals @@ -61,6 +62,19 @@ var DiscloseIndices = map[AttestationId]DiscloseIndicesEntry{ UserIdentifierIndex: 20, PassportNoSmtRootIndex: 99, }, + Aadhaar: { + RevealedDataPackedIndex: 2, + ForbiddenCountriesListPackedIndex: 6, + NullifierIndex: 0, + AttestationIdIndex: 10, + MerkleRootIndex: 16, + CurrentDateIndex: 11, + NamedobSmtRootIndex: 14, + NameyobSmtRootIndex: 15, + ScopeIndex: 17, + UserIdentifierIndex: 18, + PassportNoSmtRootIndex: 99, + }, } // Field names for revealed data @@ -140,6 +154,26 @@ var RevealedDataIndices = map[AttestationId]RevealedDataIndicesEntry{ OfacStart: 92, OfacEnd: 93, }, + Aadhaar: { + IssuingStateStart: 81, + IssuingStateEnd: 111, + NameStart: 9, + NameEnd: 70, + IdNumberStart: 71, + IdNumberEnd: 74, + NationalityStart: 999, + NationalityEnd: 999, + DateOfBirthStart: 1, + DateOfBirthEnd: 8, + GenderStart: 0, + GenderEnd: 0, + ExpiryDateStart: 999, + ExpiryDateEnd: 999, + OlderThanStart: 118, + OlderThanEnd: 118, + OfacStart: 116, + OfacEnd: 117, + }, } // AllIds contains all valid attestation IDs @@ -152,6 +186,7 @@ var AllIds = map[AttestationId]bool{ var BytesCount = map[AttestationId][]int{ Passport: {31, 31, 31}, EUCard: {31, 31, 31, 1}, + Aadhaar: {31, 31, 31, 26}, } // trimU0000 filters out null characters (\u0000) from a slice of strings @@ -280,6 +315,8 @@ func GetRevealedDataPublicSignalsLength(attestationId AttestationId) (int, error return int(93 / 31), nil case EUCard: return int(math.Ceil(94.0 / 31.0)), nil + case Aadhaar: + return int(math.Ceil(119.0 / 31.0)), nil default: return 0, fmt.Errorf("invalid attestation ID: %d", attestationId) } @@ -372,7 +409,7 @@ func FormatRevealedDataPacked(attestationID AttestationId, publicSignals PublicS // Extract forbidden countries list packed fcStartIndex := discloseIndices.ForbiddenCountriesListPackedIndex - forbiddenCountriesListPacked := publicSignals[fcStartIndex : fcStartIndex+3] + forbiddenCountriesListPacked := publicSignals[fcStartIndex : fcStartIndex+4] // Extract issuing state issuingState := string(revealedDataPackedBytes[revealedDataIndices.IssuingStateStart : revealedDataIndices.IssuingStateEnd+1]) @@ -385,33 +422,63 @@ func FormatRevealedDataPacked(attestationID AttestationId, publicSignals PublicS idNumber := string(revealedDataPackedBytes[revealedDataIndices.IdNumberStart : revealedDataIndices.IdNumberEnd+1]) // Extract nationality - nationality := string(revealedDataPackedBytes[revealedDataIndices.NationalityStart : revealedDataIndices.NationalityEnd+1]) + nationality := "" + if attestationID == Aadhaar { + nationality = "IND" + } else { + nationality = string(revealedDataPackedBytes[revealedDataIndices.NationalityStart : revealedDataIndices.NationalityEnd+1]) + } // Extract date of birth - dateOfBirth := string(revealedDataPackedBytes[revealedDataIndices.DateOfBirthStart : revealedDataIndices.DateOfBirthEnd+1]) + var dateOfBirth string + if attestationID == Aadhaar { + dobBytes := revealedDataPackedBytes[revealedDataIndices.DateOfBirthStart : revealedDataIndices.DateOfBirthEnd+1] + var dobStrings []string + for _, b := range dobBytes { + dobStrings = append(dobStrings, fmt.Sprintf("%d", int(b))) + } + dateOfBirth = strings.Join(dobStrings, "") + } else { + dateOfBirth = string(revealedDataPackedBytes[revealedDataIndices.DateOfBirthStart : revealedDataIndices.DateOfBirthEnd+1]) + } // Extract gender gender := string(revealedDataPackedBytes[revealedDataIndices.GenderStart : revealedDataIndices.GenderEnd+1]) // Extract expiry date - expiryDate := string(revealedDataPackedBytes[revealedDataIndices.ExpiryDateStart : revealedDataIndices.ExpiryDateEnd+1]) + var expiryDate string + if attestationID == Aadhaar { + expiryDate = "UNAVAILABLE" + } else { + expiryDate = string(revealedDataPackedBytes[revealedDataIndices.ExpiryDateStart : revealedDataIndices.ExpiryDateEnd+1]) + } // Extract minimum age (olderThan) - minimumAge := string(revealedDataPackedBytes[revealedDataIndices.OlderThanStart : revealedDataIndices.OlderThanEnd+1]) + var minimumAge string + if attestationID == Aadhaar { + minimumAge = string(revealedDataPackedBytes[revealedDataIndices.OlderThanStart : revealedDataIndices.OlderThanEnd+1]) + } else { + firstByte := revealedDataPackedBytes[revealedDataIndices.OlderThanStart] + minimumAge = fmt.Sprintf("%02d", int(firstByte)) + } // Extract OFAC data and convert to boolean array ofacBytes := revealedDataPackedBytes[revealedDataIndices.OfacStart : revealedDataIndices.OfacEnd+1] ofac := make([]bool, len(ofacBytes)) for i, b := range ofacBytes { - ofac[i] = b != 0 + ofac[i] = !(b != 0) + } + + if len(ofac) < 3 { + ofac = append([]bool{false}, ofac...) } // Return the structured output return GenericDiscloseOutput{ Nullifier: nullifier, ForbiddenCountriesListPacked: forbiddenCountriesListPacked, - IssuingState: issuingState, - Name: name, + IssuingState: removeNullBytes(issuingState), + Name: removeNullBytes(name), IdNumber: idNumber, Nationality: nationality, DateOfBirth: dateOfBirth, @@ -422,6 +489,11 @@ func FormatRevealedDataPacked(attestationID AttestationId, publicSignals PublicS }, nil } +// removeNullBytes removes null bytes (\x00) from a string +func removeNullBytes(str string) string { + return strings.ReplaceAll(str, "\x00", "") +} + // cleanName cleans the name string equivalent to the TypeScript regex operations // .replace(/([A-Z])<+([A-Z])/g, '$1 $2').replace(/ Date: Mon, 15 Sep 2025 18:59:42 +0530 Subject: [PATCH 03/25] chore: refactor --- sdk/sdk-go/types.go | 4 +-- sdk/sdk-go/utils.go | 62 ++++++++++++++++++++++----------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/sdk/sdk-go/types.go b/sdk/sdk-go/types.go index 206dd3af4..f552a280b 100644 --- a/sdk/sdk-go/types.go +++ b/sdk/sdk-go/types.go @@ -16,9 +16,9 @@ type VcAndDiscloseProof struct { // VerificationConfig represents the configuration for verification type VerificationConfig struct { - MinimumAge int `json:"minimumAge,omitempty"` + MinimumAge int `json:"minimumAge,omitempty"` ExcludedCountries []common.Country3LetterCode `json:"excludedCountries,omitempty"` - Ofac bool `json:"ofac,omitempty"` + Ofac bool `json:"ofac,omitempty"` } // IsValidDetails contains the validation results diff --git a/sdk/sdk-go/utils.go b/sdk/sdk-go/utils.go index 5e673391c..1334fc73a 100644 --- a/sdk/sdk-go/utils.go +++ b/sdk/sdk-go/utils.go @@ -63,18 +63,18 @@ var DiscloseIndices = map[AttestationId]DiscloseIndicesEntry{ PassportNoSmtRootIndex: 99, }, Aadhaar: { - RevealedDataPackedIndex: 2, - ForbiddenCountriesListPackedIndex: 6, - NullifierIndex: 0, - AttestationIdIndex: 10, - MerkleRootIndex: 16, - CurrentDateIndex: 11, - NamedobSmtRootIndex: 14, - NameyobSmtRootIndex: 15, - ScopeIndex: 17, - UserIdentifierIndex: 18, - PassportNoSmtRootIndex: 99, - }, + RevealedDataPackedIndex: 2, + ForbiddenCountriesListPackedIndex: 6, + NullifierIndex: 0, + AttestationIdIndex: 10, + MerkleRootIndex: 16, + CurrentDateIndex: 11, + NamedobSmtRootIndex: 14, + NameyobSmtRootIndex: 15, + ScopeIndex: 17, + UserIdentifierIndex: 18, + PassportNoSmtRootIndex: 99, + }, } // Field names for revealed data @@ -155,25 +155,25 @@ var RevealedDataIndices = map[AttestationId]RevealedDataIndicesEntry{ OfacEnd: 93, }, Aadhaar: { - IssuingStateStart: 81, - IssuingStateEnd: 111, - NameStart: 9, - NameEnd: 70, - IdNumberStart: 71, - IdNumberEnd: 74, - NationalityStart: 999, - NationalityEnd: 999, - DateOfBirthStart: 1, - DateOfBirthEnd: 8, - GenderStart: 0, - GenderEnd: 0, - ExpiryDateStart: 999, - ExpiryDateEnd: 999, - OlderThanStart: 118, - OlderThanEnd: 118, - OfacStart: 116, - OfacEnd: 117, - }, + IssuingStateStart: 81, + IssuingStateEnd: 111, + NameStart: 9, + NameEnd: 70, + IdNumberStart: 71, + IdNumberEnd: 74, + NationalityStart: 999, + NationalityEnd: 999, + DateOfBirthStart: 1, + DateOfBirthEnd: 8, + GenderStart: 0, + GenderEnd: 0, + ExpiryDateStart: 999, + ExpiryDateEnd: 999, + OlderThanStart: 118, + OlderThanEnd: 118, + OfacStart: 116, + OfacEnd: 117, + }, } // AllIds contains all valid attestation IDs From b1dc0ba4b235b9b39d611047d8a7d8e146a48526 Mon Sep 17 00:00:00 2001 From: Leszek Stachowski Date: Thu, 11 Sep 2025 15:48:12 +0200 Subject: [PATCH 04/25] move clearPassportData, markCurrentDocumentAsRegistered, reStorePassportDataWithRightCSCA to SDK (#1041) --- app/jest.config.cjs | 6 +- app/src/providers/passportDataProvider.tsx | 93 +++--------- app/src/utils/proving/provingMachine.ts | 25 ++-- app/vite.config.ts | 5 +- common/src/utils/index.ts | 2 + common/src/utils/passports/passport.ts | 34 ++++- packages/mobile-sdk-alpha/src/browser.ts | 11 +- packages/mobile-sdk-alpha/src/client.ts | 8 +- .../mobile-sdk-alpha/src/documents/utils.ts | 132 ++++++++++++++++++ packages/mobile-sdk-alpha/src/index.ts | 13 +- packages/mobile-sdk-alpha/src/types/public.ts | 12 +- .../mobile-sdk-alpha/tests/client.test.ts | 2 + .../tests/documents/utils.test.ts | 8 ++ .../tests/utils/testHelpers.ts | 2 + 14 files changed, 258 insertions(+), 95 deletions(-) diff --git a/app/jest.config.cjs b/app/jest.config.cjs index 6f3e31533..2bfd94433 100644 --- a/app/jest.config.cjs +++ b/app/jest.config.cjs @@ -6,7 +6,7 @@ module.exports = { preset: 'react-native', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], transformIgnorePatterns: [ - 'node_modules/(?!(react-native|@react-native|@react-navigation|@react-native-community|@segment/analytics-react-native|@openpassport|react-native-keychain|react-native-check-version|react-native-nfc-manager|react-native-passport-reader|react-native-gesture-handler|uuid|@stablelib|@react-native-google-signin|react-native-cloud-storage|@react-native-clipboard|@react-native-firebase|@selfxyz|@sentry)/)', + 'node_modules/(?!(react-native|@react-native|@react-navigation|@react-native-community|@segment/analytics-react-native|@openpassport|react-native-keychain|react-native-check-version|react-native-nfc-manager|react-native-passport-reader|react-native-gesture-handler|uuid|@stablelib|@react-native-google-signin|react-native-cloud-storage|@react-native-clipboard|@react-native-firebase|@selfxyz|@sentry|@anon-aadhaar)/)', ], setupFiles: ['/jest.setup.js'], testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$', @@ -21,6 +21,10 @@ module.exports = { '/../packages/mobile-sdk-alpha/dist/cjs/index.cjs', '^@selfxyz/mobile-sdk-alpha/(.*)$': '/../packages/mobile-sdk-alpha/dist/cjs/$1.cjs', + // Fix snarkjs resolution for @anon-aadhaar/core + '^snarkjs$': '/../node_modules/snarkjs/build/main.cjs', + // Fix ffjavascript resolution for snarkjs dependencies + '^ffjavascript$': '/../node_modules/ffjavascript/build/main.cjs', }, globals: { 'ts-jest': { diff --git a/app/src/providers/passportDataProvider.tsx b/app/src/providers/passportDataProvider.tsx index 4224cee34..6c0effd2c 100644 --- a/app/src/providers/passportDataProvider.tsx +++ b/app/src/providers/passportDataProvider.tsx @@ -40,7 +40,6 @@ * - Display format determined by documentCategory */ -import { sha256 } from 'js-sha256'; import type { PropsWithChildren } from 'react'; import React, { createContext, useCallback, useContext, useMemo } from 'react'; import Keychain from 'react-native-keychain'; @@ -51,11 +50,12 @@ import type { } from '@selfxyz/common/utils'; import { brutforceSignatureAlgorithmDsc, + calculateContentHash, + inferDocumentCategory, parseCertificateSimple, } from '@selfxyz/common/utils'; import type { DocumentCatalog, - DocumentCategory, DocumentMetadata, PassportData, } from '@selfxyz/common/utils/types'; @@ -113,35 +113,6 @@ const notifyDocumentChange = (isMock: boolean) => { // ===== NEW STORAGE IMPLEMENTATION ===== -function calculateContentHash(passportData: PassportData): string { - if (passportData.eContent) { - // eContent is likely a buffer or array, convert to string properly - const eContentStr = - typeof passportData.eContent === 'string' - ? passportData.eContent - : JSON.stringify(passportData.eContent); - return sha256(eContentStr); - } - // For documents without eContent (like aadhaar), hash core stable fields - const stableData = { - documentType: passportData.documentType, - data: passportData.mrz || '', // Use mrz for passports/IDs, could be other data for aadhaar - documentCategory: passportData.documentCategory, - }; - return sha256(JSON.stringify(stableData)); -} - -function inferDocumentCategory(documentType: string): DocumentCategory { - if (documentType.includes('passport')) { - return 'passport' as DocumentCategory; - } else if (documentType.includes('id')) { - return 'id_card' as DocumentCategory; - } else if (documentType.includes('aadhaar')) { - return 'aadhaar' as DocumentCategory; - } - return 'passport' as DocumentCategory; // fallback -} - // Global flag to track if native modules are ready let nativeModulesReady = false; @@ -153,7 +124,6 @@ export const PassportContext = createContext({ setData: storePassportData, getPassportDataAndSecret: () => Promise.resolve(null), getSelectedPassportDataAndSecret: () => Promise.resolve(null), - clearPassportData: clearPassportData, clearSpecificData: clearSpecificPassportData, loadDocumentCatalog: safeLoadDocumentCatalog, getAllDocuments: () => Promise.resolve({}), @@ -163,7 +133,6 @@ export const PassportContext = createContext({ getCurrentDocumentType: getCurrentDocumentType, clearDocumentCatalogForMigrationTesting: clearDocumentCatalogForMigrationTesting, - markCurrentDocumentAsRegistered: markCurrentDocumentAsRegistered, updateDocumentRegistrationState: updateDocumentRegistrationState, checkIfAnyDocumentsNeedMigration: checkIfAnyDocumentsNeedMigration, checkAndUpdateRegistrationStates: checkAndUpdateRegistrationStates, @@ -217,7 +186,6 @@ export const PassportProvider = ({ children }: PassportProviderProps) => { setData: storePassportData, getPassportDataAndSecret, getSelectedPassportDataAndSecret, - clearPassportData: clearPassportData, clearSpecificData: clearSpecificPassportData, loadDocumentCatalog: safeLoadDocumentCatalog, getAllDocuments: () => safeGetAllDocuments(selfClient), @@ -227,7 +195,6 @@ export const PassportProvider = ({ children }: PassportProviderProps) => { getCurrentDocumentType: getCurrentDocumentType, clearDocumentCatalogForMigrationTesting: clearDocumentCatalogForMigrationTesting, - markCurrentDocumentAsRegistered: markCurrentDocumentAsRegistered, updateDocumentRegistrationState: updateDocumentRegistrationState, checkIfAnyDocumentsNeedMigration: checkIfAnyDocumentsNeedMigration, checkAndUpdateRegistrationStates: checkAndUpdateRegistrationStates, @@ -296,22 +263,6 @@ export async function clearDocumentCatalogForMigrationTesting() { ); } -export async function clearPassportData() { - const catalog = await loadDocumentCatalogDirectlyFromKeychain(); - - // Delete all documents - for (const doc of catalog.documents) { - try { - await Keychain.resetGenericPassword({ service: `document-${doc.id}` }); - } catch { - console.log(`Document ${doc.id} not found or already cleared`); - } - } - - // Clear catalog - await saveDocumentCatalogDirectlyToKeychain({ documents: [] }); -} - export async function clearSpecificPassportData(documentType: string) { const catalog = await loadDocumentCatalogDirectlyFromKeychain(); const docsToDelete = catalog.documents.filter( @@ -323,6 +274,12 @@ export async function clearSpecificPassportData(documentType: string) { } } +export async function deleteDocumentDirectlyFromKeychain( + documentId: string, +): Promise { + await Keychain.resetGenericPassword({ service: `document-${documentId}` }); +} + export async function deleteDocument(documentId: string): Promise { const catalog = await loadDocumentCatalogDirectlyFromKeychain(); @@ -472,6 +429,8 @@ export const selfClientDocumentsAdapter: DocumentsAdapter = { loadDocumentCatalog: loadDocumentCatalogDirectlyFromKeychain, loadDocumentById: loadDocumentByIdDirectlyFromKeychain, saveDocumentCatalog: saveDocumentCatalogDirectlyToKeychain, + deleteDocument: deleteDocumentDirectlyFromKeychain, + saveDocument: storeDocumentDirectlyToKeychain, }; export async function loadDocumentCatalogDirectlyFromKeychain(): Promise { @@ -653,7 +612,6 @@ interface IPassportContext { data: { passportData: PassportData; secret: string }; signature: string; } | null>; - clearPassportData: () => Promise; clearSpecificData: (documentType: string) => Promise; loadDocumentCatalog: () => Promise; @@ -667,7 +625,6 @@ interface IPassportContext { migrateFromLegacyStorage: () => Promise; getCurrentDocumentType: () => Promise; clearDocumentCatalogForMigrationTesting: () => Promise; - markCurrentDocumentAsRegistered: () => Promise; updateDocumentRegistrationState: ( documentId: string, isRegistered: boolean, @@ -676,15 +633,6 @@ interface IPassportContext { checkAndUpdateRegistrationStates: () => Promise; } -export async function markCurrentDocumentAsRegistered(): Promise { - const catalog = await loadDocumentCatalogDirectlyFromKeychain(); - if (catalog.selectedDocumentId) { - await updateDocumentRegistrationState(catalog.selectedDocumentId, true); - } else { - console.warn('No selected document to mark as registered'); - } -} - export async function migrateFromLegacyStorage(): Promise { console.log('Migrating from legacy storage to new architecture...'); const catalog = await loadDocumentCatalogDirectlyFromKeychain(); @@ -792,6 +740,15 @@ export async function setSelectedDocument(documentId: string): Promise { } } +async function storeDocumentDirectlyToKeychain( + contentHash: string, + passportData: PassportData, +): Promise { + await Keychain.setGenericPassword(contentHash, JSON.stringify(passportData), { + service: `document-${contentHash}`, + }); +} + export async function storeDocumentWithDeduplication( passportData: PassportData, ): Promise { @@ -806,13 +763,7 @@ export async function storeDocumentWithDeduplication( console.log('Document with same content exists, updating stored data'); // Update the stored document with potentially new metadata - await Keychain.setGenericPassword( - contentHash, - JSON.stringify(passportData), - { - service: `document-${contentHash}`, - }, - ); + await storeDocumentDirectlyToKeychain(contentHash, passportData); // Update selected document to this one catalog.selectedDocumentId = contentHash; @@ -821,9 +772,7 @@ export async function storeDocumentWithDeduplication( } // Store new document using contentHash as service name - await Keychain.setGenericPassword(contentHash, JSON.stringify(passportData), { - service: `document-${contentHash}`, - }); + await storeDocumentDirectlyToKeychain(contentHash, passportData); // Add to catalog const metadata: DocumentMetadata = { diff --git a/app/src/utils/proving/provingMachine.ts b/app/src/utils/proving/provingMachine.ts index 834c6f171..f7df25b51 100644 --- a/app/src/utils/proving/provingMachine.ts +++ b/app/src/utils/proving/provingMachine.ts @@ -37,8 +37,11 @@ import { getWSDbRelayerUrl, } from '@selfxyz/common/utils/proving'; import { + clearPassportData, hasAnyValidRegisteredDocument, loadSelectedDocument, + markCurrentDocumentAsRegistered, + reStorePassportDataWithRightCSCA, SdkEvents, SelfClient, } from '@selfxyz/mobile-sdk-alpha'; @@ -48,12 +51,6 @@ import { } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; import { useProtocolStore } from '@selfxyz/mobile-sdk-alpha/stores'; -// will need to be passed in from selfClient -import { - clearPassportData, - markCurrentDocumentAsRegistered, - reStorePassportDataWithRightCSCA, -} from '@/providers/passportDataProvider'; import { useSelfAppStore } from '@/stores/selfAppStore'; import analytics from '@/utils/analytics'; import { generateTEEInputsDisclose } from '@/utils/proving/provingInputs'; @@ -261,7 +258,7 @@ export const useProvingStore = create((set, get) => { if (get().circuitType === 'register') { (async () => { try { - await markCurrentDocumentAsRegistered(); + await markCurrentDocumentAsRegistered(selfClient); } catch (error) { //This will be checked and updated when the app launches the next time console.error('Error marking document as registered:', error); @@ -695,7 +692,7 @@ export const useProvingStore = create((set, get) => { } }, - validatingDocument: async (_selfClient: SelfClient) => { + validatingDocument: async (selfClient: SelfClient) => { _checkActorInitialized(actor); // TODO: for the disclosure, we could check that the selfApp is a valid one. trackEvent(ProofEvents.VALIDATION_STARTED); @@ -718,7 +715,9 @@ export const useProvingStore = create((set, get) => { status: isSupported.status, details: isSupported.details, }); - await clearPassportData(); + + await clearPassportData(selfClient); + actor!.send({ type: 'PASSPORT_NOT_SUPPORTED' }); return; } @@ -755,12 +754,16 @@ export const useProvingStore = create((set, get) => { }, ); if (isRegistered) { - reStorePassportDataWithRightCSCA(passportData, csca as string); + await reStorePassportDataWithRightCSCA( + selfClient, + passportData, + csca as string, + ); // Mark document as registered since its already onChain (async () => { try { - await markCurrentDocumentAsRegistered(); + await markCurrentDocumentAsRegistered(selfClient); } catch (error) { //it will be checked and marked as registered during next app launch console.error('Error marking document as registered:', error); diff --git a/app/vite.config.ts b/app/vite.config.ts index 686842fa8..73f4b6ec1 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -50,6 +50,7 @@ export default defineConfig({ __dirname, 'src/mocks/react-native-passport-reader.ts', ), + crypto: 'crypto-browserify', }, }, plugins: [ @@ -84,7 +85,7 @@ export default defineConfig({ global: 'globalThis', }, optimizeDeps: { - exclude: ['fs', 'path', 'child_process'], + exclude: ['fs', 'path', 'child_process', '@zk-email/helpers'], esbuildOptions: { // Optimize minification minifyIdentifiers: true, @@ -102,7 +103,7 @@ export default defineConfig({ cssMinify: true, cssCodeSplit: true, rollupOptions: { - external: ['fs', 'path', 'child_process'], + external: ['fs', 'child_process'], output: { // Optimize chunk size and minification compact: true, diff --git a/common/src/utils/index.ts b/common/src/utils/index.ts index 571b05247..e9fcef915 100644 --- a/common/src/utils/index.ts +++ b/common/src/utils/index.ts @@ -41,6 +41,8 @@ export { generateCommitment, generateNullifier, initPassportDataParsing, + calculateContentHash, + inferDocumentCategory, } from './passports/passport.js'; export { formatMrz } from './passports/format.js'; export { genAndInitMockPassportData } from './passports/genMockPassportData.js'; diff --git a/common/src/utils/passports/passport.ts b/common/src/utils/passports/passport.ts index 0c573cb57..15da0fb1a 100644 --- a/common/src/utils/passports/passport.ts +++ b/common/src/utils/passports/passport.ts @@ -29,9 +29,10 @@ import { findStartIndex, findStartIndexEC } from '../csca.js'; import { hash, packBytesAndPoseidon } from '../hash.js'; import { sha384_512Pad, shaPad } from '../shaPad.js'; import { getLeafDscTree } from '../trees.js'; -import type { PassportData, SignatureAlgorithm } from '../types.js'; +import type { DocumentCategory, PassportData, SignatureAlgorithm } from '../types.js'; import { formatMrz } from './format.js'; import { parsePassportData } from './passport_parsing/parsePassportData.js'; +import { sha256 } from 'js-sha256'; export function extractRSFromSignature(signatureBytes: number[]): { r: string; s: string } { const derSignature = Buffer.from(signatureBytes).toString('binary'); @@ -306,3 +307,34 @@ export function pad(hashFunction: (typeof hashAlgos)[number]) { export function padWithZeroes(bytes: number[], length: number) { return bytes.concat(new Array(length - bytes.length).fill(0)); } + +export function calculateContentHash(passportData: PassportData): string { + if (passportData.eContent) { + // eContent is likely a buffer or array, convert to string properly + const eContentStr = + typeof passportData.eContent === 'string' + ? passportData.eContent + : JSON.stringify(passportData.eContent); + + return sha256(eContentStr); + } + // For documents without eContent (like aadhaar), hash core stable fields + const stableData = { + documentType: passportData.documentType, + data: passportData.mrz || '', // Use mrz for passports/IDs, could be other data for aadhaar + documentCategory: passportData.documentCategory, + }; + + return sha256(JSON.stringify(stableData)); +} + +export function inferDocumentCategory(documentType: string): DocumentCategory { + if (documentType.includes('passport')) { + return 'passport' as DocumentCategory; + } else if (documentType.includes('id')) { + return 'id_card' as DocumentCategory; + } else if (documentType.includes('aadhaar')) { + return 'aadhaar' as DocumentCategory; + } + return 'passport' as DocumentCategory; // fallback +} diff --git a/packages/mobile-sdk-alpha/src/browser.ts b/packages/mobile-sdk-alpha/src/browser.ts index cfee8c608..5810a4bcc 100644 --- a/packages/mobile-sdk-alpha/src/browser.ts +++ b/packages/mobile-sdk-alpha/src/browser.ts @@ -45,6 +45,15 @@ export { SdkEvents } from './types/events'; export { SelfClientContext, SelfClientProvider, useSelfClient } from './context'; +export { + clearPassportData, + getAllDocuments, + hasAnyValidRegisteredDocument, + loadSelectedDocument, + markCurrentDocumentAsRegistered, + reStorePassportDataWithRightCSCA, +} from './documents/utils'; + export { createListenersMap, createSelfClient } from './client'; export { defaultConfig } from './config/defaults'; @@ -54,8 +63,6 @@ export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz'; export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator'; -export { getAllDocuments, hasAnyValidRegisteredDocument, loadSelectedDocument } from './documents/utils'; - // Core functions export { isPassportDataValid } from './validation/document'; diff --git a/packages/mobile-sdk-alpha/src/client.ts b/packages/mobile-sdk-alpha/src/client.ts index 88c164554..03f04c16c 100644 --- a/packages/mobile-sdk-alpha/src/client.ts +++ b/packages/mobile-sdk-alpha/src/client.ts @@ -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/utils/types'; +import type { DocumentCatalog, PassportData } from '@selfxyz/common/utils/types'; import { defaultConfig } from './config/defaults'; import { mergeConfig } from './config/merge'; @@ -198,5 +198,11 @@ export function createSelfClient({ saveDocumentCatalog: async (catalog: DocumentCatalog) => { return _adapters.documents.saveDocumentCatalog(catalog); }, + deleteDocument: async (id: string) => { + return _adapters.documents.deleteDocument(id); + }, + saveDocument: async (id: string, passportData: PassportData) => { + return _adapters.documents.saveDocument(id, passportData); + }, }; } diff --git a/packages/mobile-sdk-alpha/src/documents/utils.ts b/packages/mobile-sdk-alpha/src/documents/utils.ts index 31e77bf3f..18fb59618 100644 --- a/packages/mobile-sdk-alpha/src/documents/utils.ts +++ b/packages/mobile-sdk-alpha/src/documents/utils.ts @@ -2,10 +2,33 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. +import { + brutforceSignatureAlgorithmDsc, + parseCertificateSimple, + PublicKeyDetailsECDSA, + PublicKeyDetailsRSA, +} from '@selfxyz/common'; +import { calculateContentHash, inferDocumentCategory } from '@selfxyz/common/utils'; import { DocumentMetadata, PassportData } from '@selfxyz/common/utils/types'; import { SelfClient } from '../types/public'; +export async function clearPassportData(selfClient: SelfClient) { + const catalog = await selfClient.loadDocumentCatalog(); + + // Delete all documents + for (const doc of catalog.documents) { + try { + await selfClient.deleteDocument(doc.id); + } catch { + console.log(`Document ${doc.id} not found or already cleared`); + } + } + + // Clear catalog + await selfClient.saveDocumentCatalog({ documents: [] }); +} + /** * Gets all documents from the document catalog. * @@ -88,3 +111,112 @@ export const loadSelectedDocument = async ( console.log('Successfully loaded document:', metadata.documentType); return { data, metadata }; }; + +export async function markCurrentDocumentAsRegistered(selfClient: SelfClient): Promise { + const catalog = await selfClient.loadDocumentCatalog(); + + if (catalog.selectedDocumentId) { + await updateDocumentRegistrationState(selfClient, catalog.selectedDocumentId, true); + } else { + console.warn('No selected document to mark as registered'); + } +} + +export async function reStorePassportDataWithRightCSCA( + selfClient: SelfClient, + passportData: PassportData, + csca: string, +) { + const cscaInCurrentPassporData = passportData.passportMetadata?.csca; + if (!(csca === cscaInCurrentPassporData)) { + const cscaParsed = parseCertificateSimple(csca); + const dscCertData = brutforceSignatureAlgorithmDsc(passportData.dsc_parsed!, cscaParsed); + + if (passportData.passportMetadata && dscCertData && cscaParsed.publicKeyDetails) { + passportData.passportMetadata.csca = csca; + passportData.passportMetadata.cscaFound = true; + passportData.passportMetadata.cscaHashFunction = dscCertData.hashAlgorithm; + passportData.passportMetadata.cscaSignatureAlgorithm = dscCertData.signatureAlgorithm; + passportData.passportMetadata.cscaSaltLength = dscCertData.saltLength; + + const cscaCurveOrExponent = + cscaParsed.signatureAlgorithm === 'rsapss' || cscaParsed.signatureAlgorithm === 'rsa' + ? (cscaParsed.publicKeyDetails as PublicKeyDetailsRSA).exponent + : (cscaParsed.publicKeyDetails as PublicKeyDetailsECDSA).curve; + + passportData.passportMetadata.cscaCurveOrExponent = cscaCurveOrExponent; + passportData.passportMetadata.cscaSignatureAlgorithmBits = parseInt(cscaParsed.publicKeyDetails.bits, 10); + + passportData.csca_parsed = cscaParsed; + + await storePassportData(selfClient, passportData); + } + } +} + +export async function storeDocumentWithDeduplication( + selfClient: SelfClient, + passportData: PassportData, +): Promise { + const contentHash = calculateContentHash(passportData); + const catalog = await selfClient.loadDocumentCatalog(); + + // Check for existing document with same content + const existing = catalog.documents.find(d => d.id === contentHash); + if (existing) { + // Even if content hash is the same, we should update the document + // in case metadata (like CSCA) has changed + console.log('Document with same content exists, updating stored data'); + + // Update the stored document with potentially new metadata + await selfClient.saveDocument(contentHash, passportData); + + // Update selected document to this one + catalog.selectedDocumentId = contentHash; + await selfClient.saveDocumentCatalog(catalog); + return contentHash; + } + + // Store new document using contentHash as service name + await selfClient.saveDocument(contentHash, passportData); + + // Add to catalog + const metadata: DocumentMetadata = { + id: contentHash, + documentType: passportData.documentType, + documentCategory: passportData.documentCategory || inferDocumentCategory(passportData.documentType), + data: passportData.mrz || '', // Store MRZ for passports/IDs, relevant data for aadhaar + mock: passportData.mock || false, + isRegistered: false, + }; + + catalog.documents.push(metadata); + catalog.selectedDocumentId = contentHash; + + await selfClient.saveDocumentCatalog(catalog); + + return contentHash; +} + +export async function storePassportData(selfClient: SelfClient, passportData: PassportData) { + await storeDocumentWithDeduplication(selfClient, passportData); +} + +export async function updateDocumentRegistrationState( + selfClient: SelfClient, + documentId: string, + isRegistered: boolean, +): Promise { + const catalog = await selfClient.loadDocumentCatalog(); + const documentIndex = catalog.documents.findIndex(d => d.id === documentId); + + if (documentIndex !== -1) { + catalog.documents[documentIndex].isRegistered = isRegistered; + + await selfClient.saveDocumentCatalog(catalog); + + console.log(`Updated registration state for document ${documentId}: ${isRegistered}`); + } else { + console.warn(`Document ${documentId} not found in catalog`); + } +} diff --git a/packages/mobile-sdk-alpha/src/index.ts b/packages/mobile-sdk-alpha/src/index.ts index cfd13c70f..afc18791a 100644 --- a/packages/mobile-sdk-alpha/src/index.ts +++ b/packages/mobile-sdk-alpha/src/index.ts @@ -77,6 +77,16 @@ export { SelfClientContext, SelfClientProvider, useSelfClient } from './context' // Components export { SelfMobileSdk } from './entry'; +// Documents utils +export { + clearPassportData, + getAllDocuments, + hasAnyValidRegisteredDocument, + loadSelectedDocument, + markCurrentDocumentAsRegistered, + reStorePassportDataWithRightCSCA, +} from './documents/utils'; + export { createListenersMap, createSelfClient } from './client'; export { defaultConfig } from './config/defaults'; @@ -88,9 +98,6 @@ export { formatDateToYYMMDD, scanMRZ } from './mrz'; export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator'; -// Documents utils -export { getAllDocuments, hasAnyValidRegisteredDocument, loadSelectedDocument } from './documents/utils'; - // Core functions export { isPassportDataValid } from './validation/document'; diff --git a/packages/mobile-sdk-alpha/src/types/public.ts b/packages/mobile-sdk-alpha/src/types/public.ts index ea9e17487..e7125886f 100644 --- a/packages/mobile-sdk-alpha/src/types/public.ts +++ b/packages/mobile-sdk-alpha/src/types/public.ts @@ -176,8 +176,12 @@ export interface ScannerAdapter { export interface DocumentsAdapter { loadDocumentCatalog(): Promise; - loadDocumentById(id: string): Promise; saveDocumentCatalog(catalog: DocumentCatalog): Promise; + + loadDocumentById(id: string): Promise; + saveDocument(id: string, passportData: PassportData): Promise; + + deleteDocument(id: string): Promise; } export interface SelfClient { @@ -201,8 +205,12 @@ export interface SelfClient { emit(event: E, payload?: SDKEventMap[E]): void; loadDocumentCatalog(): Promise; - loadDocumentById(id: string): Promise; saveDocumentCatalog(catalog: DocumentCatalog): Promise; + + loadDocumentById(id: string): Promise; + saveDocument(id: string, passportData: PassportData): Promise; + + deleteDocument(id: string): Promise; } export type Unsubscribe = () => void; export interface StorageAdapter { diff --git a/packages/mobile-sdk-alpha/tests/client.test.ts b/packages/mobile-sdk-alpha/tests/client.test.ts index 075575473..133691f01 100644 --- a/packages/mobile-sdk-alpha/tests/client.test.ts +++ b/packages/mobile-sdk-alpha/tests/client.test.ts @@ -231,4 +231,6 @@ const documents: DocumentsAdapter = { loadDocumentCatalog: async () => ({ documents: [] }), loadDocumentById: async () => null, saveDocumentCatalog: async () => {}, + saveDocument: async () => {}, + deleteDocument: async () => {}, }; diff --git a/packages/mobile-sdk-alpha/tests/documents/utils.test.ts b/packages/mobile-sdk-alpha/tests/documents/utils.test.ts index a1f080652..9c4d6eb97 100644 --- a/packages/mobile-sdk-alpha/tests/documents/utils.test.ts +++ b/packages/mobile-sdk-alpha/tests/documents/utils.test.ts @@ -82,6 +82,8 @@ describe('loadSelectedDocument', () => { loadDocumentCatalog: loadDocumentCatalogSpy, loadDocumentById: loadDocumentByIdSpy, saveDocumentCatalog: saveDocumentCatalogSpy, + saveDocument: vi.fn(), + deleteDocument: vi.fn(), }); const document = await loadSelectedDocument(client); @@ -100,6 +102,8 @@ describe('loadSelectedDocument', () => { loadDocumentCatalog: loadDocumentCatalogSpy, loadDocumentById: loadDocumentByIdSpy, saveDocumentCatalog: saveDocumentCatalogSpy, + saveDocument: vi.fn(), + deleteDocument: vi.fn(), }); const document = await loadSelectedDocument(client); @@ -121,6 +125,8 @@ describe('loadSelectedDocument', () => { loadDocumentCatalog: loadDocumentCatalogSpy, loadDocumentById: loadDocumentByIdSpy, saveDocumentCatalog: saveDocumentCatalogSpy, + saveDocument: vi.fn(), + deleteDocument: vi.fn(), }); const document = await loadSelectedDocument(client); @@ -142,6 +148,8 @@ describe('loadSelectedDocument', () => { loadDocumentCatalog: loadDocumentCatalogSpy, loadDocumentById: loadDocumentByIdSpy, saveDocumentCatalog: saveDocumentCatalogSpy, + saveDocument: vi.fn(), + deleteDocument: vi.fn(), }); const document = await loadSelectedDocument(client); diff --git a/packages/mobile-sdk-alpha/tests/utils/testHelpers.ts b/packages/mobile-sdk-alpha/tests/utils/testHelpers.ts index 105ff8b3b..5b437a0a4 100644 --- a/packages/mobile-sdk-alpha/tests/utils/testHelpers.ts +++ b/packages/mobile-sdk-alpha/tests/utils/testHelpers.ts @@ -57,6 +57,8 @@ export const mockDocuments: DocumentsAdapter = { loadDocumentCatalog: async () => ({ documents: [] }), loadDocumentById: async () => null, saveDocumentCatalog: () => Promise.resolve(), + saveDocument: () => Promise.resolve(), + deleteDocument: () => Promise.resolve(), }; const mockAuth = { From 761fb2fe8eda0a854fe46a054a182d9a7a3b1c07 Mon Sep 17 00:00:00 2001 From: Aaron DeRuvo Date: Thu, 11 Sep 2025 17:30:01 +0200 Subject: [PATCH 05/25] Move self app store to mobile sdk (#1040) --- app/README.md | 37 +- app/package.json | 4 +- app/src/components/NavBar/HomeNavBar.tsx | 2 +- .../screens/prove/ConfirmBelongingScreen.tsx | 4 +- .../prove/ProofRequestStatusScreen.tsx | 2 +- app/src/screens/prove/ProveScreen.tsx | 4 +- .../screens/prove/QRCodeViewFinderScreen.tsx | 2 +- app/src/utils/deeplinks.ts | 2 +- app/src/utils/proving/provingInputs.ts | 31 -- app/src/utils/proving/provingMachine.ts | 275 ++++++++------- app/tests/utils/deeplinks.test.ts | 2 +- .../provingMachine.generatePayload.test.ts | 99 ++++-- .../provingMachine.startFetchingData.test.ts | 6 +- .../utils/proving/provingMachine.test.ts | 2 +- app/vite.config.ts | 1 - common/index.ts | 25 +- common/package.json | 4 +- common/src/constants/constants.ts | 4 +- .../utils/aadhaar/build_aadhaar_ofac_smt.ts | 1 + common/src/utils/aadhaar/constants.ts | 1 + common/src/utils/aadhaar/mockData.ts | 283 +++++++-------- common/src/utils/aadhaar/utils.ts | 326 +++++++++--------- common/src/utils/bytes.ts | 4 +- common/src/utils/hash.test.ts | 45 +++ common/src/utils/hash.ts | 5 +- common/src/utils/trees.ts | 180 +++++----- package.json | 5 +- packages/mobile-sdk-alpha/package.json | 5 +- packages/mobile-sdk-alpha/src/browser.ts | 3 +- packages/mobile-sdk-alpha/src/index.ts | 4 + .../processing/generate-disclosure-inputs.ts | 30 ++ packages/mobile-sdk-alpha/src/stores/index.ts | 1 + .../src/stores/selfAppStore.tsx | 23 +- .../generate-disclosure-inputs.test.ts | 148 ++++++++ yarn.lock | 2 + 35 files changed, 930 insertions(+), 642 deletions(-) delete mode 100644 app/src/utils/proving/provingInputs.ts create mode 100644 common/src/utils/hash.test.ts create mode 100644 packages/mobile-sdk-alpha/src/processing/generate-disclosure-inputs.ts rename {app => packages/mobile-sdk-alpha}/src/stores/selfAppStore.tsx (88%) create mode 100644 packages/mobile-sdk-alpha/tests/processing/generate-disclosure-inputs.test.ts diff --git a/app/README.md b/app/README.md index 26e4b8ec8..842e1aa5e 100644 --- a/app/README.md +++ b/app/README.md @@ -12,12 +12,12 @@ ### Android -| Requirement | Version | Installation Guide | -| --------------------------- | ------------- | ------------------------------------------------------------------------------------- | -| Java | 17 | [Install Java](https://www.oracle.com/java/technologies/javase-jdk17-downloads.html) | -| Android Studio (Optional)* | Latest | [Install Android Studio](https://developer.android.com/studio) | -| Android SDK | Latest | See instructions for Android below | -| Android NDK | 27.0.11718014 | See instructions for Android below | +| Requirement | Version | Installation Guide | +| --------------------------- | ------------- | ------------------------------------------------------------------------------------ | +| Java | 17 | [Install Java](https://www.oracle.com/java/technologies/javase-jdk17-downloads.html) | +| Android Studio (Optional)\* | Latest | [Install Android Studio](https://developer.android.com/studio) | +| Android SDK | Latest | See instructions for Android below | +| Android NDK | 27.0.11718014 | See instructions for Android below | \* To facilitate the installation of the SDK and the NDK, and to pair with development devices with a conventient QR code, you can use Android Studio. @@ -62,39 +62,40 @@ Under **SDK Platforms**, install the platform with the highest API number Under **SDK Tools**, check the **Show Package Details** checkbox, expand **NDK (Side by side)**, select version **27.0.11718014** and install. - #### Using sdkmanager via CLI Create a directory for the Android SDK. For example `~/android_sdk`. Define the environment variable `ANDROID_HOME` to point that directory. Install sdkmanager under `ANDROID_HOME` according to the instructions on https://developer.android.com/tools/sdkmanager - - List available SDK platforms + ```bash $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --list | grep platforms ``` -In the list of platforms, find the latest version and install it. (Replace *NN* with the latest version number) +In the list of platforms, find the latest version and install it. (Replace _NN_ with the latest version number) + ```bash $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install "platforms;android-NN" ``` Install the NDK + ```bash $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11718014" ``` + Define the environment variable `ANDROID_NDK_VERSION` to `27.0.11718014` and `ANDROID_NDK` to `$ANDROID_HOME/ndk/27.0.11718014` Install Platform Tools, needed for the `adb` tool + ```bash $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install platform-tools ``` Add `$ANDROID_HOME/platform-tools` to your `$PATH` variable - ## Run the app ### Android @@ -108,11 +109,13 @@ In Android Studio, use Device Manager to pair with and connect to your phone. ##### Using adb In your phone's developer settings, select **Wireless debugging** > **Pair the device using a pairing code**. Using the displayed information, run + ``` adb pair PHONE_IP:PAIRING_PORT PAIRING_CODE ``` To connect to the device, find the IP number and port (different port than in the pairing step) directly under Wireless debugging, and run + ``` adb connect PHONE_IP:DEVELOPMENT_PORT ``` @@ -126,11 +129,11 @@ sdk.dir=/path/to/your/android/sdk ``` or create it with + ```bash echo sdk.dir=$ANDROID_HOME > android/local.properties ``` - Launch the React Native server: ```bash @@ -160,6 +163,7 @@ pod install And run the app in Xcode. #### Simulator Build + > **Note:** iOS Simulator on Apple Silicon Macs requires Rosetta (x86_64) mode due to simulator architecture compatibility. If you're using a Silicon Mac (M1/M2/M3/M4), you may find that the Rosetta simulator build option is not available by default in Xcode. To enable it, open Xcode and go to **Product > Destination > Show All Run Destinations**. This will unlock the ability to select the Rosetta build simulator, allowing you to run the app in the iOS Simulator. @@ -235,6 +239,7 @@ Deployments happen automatically when you merge PRs: 2. **Merge to `main`** → Deploys to production To control versions with PR labels: + - `version:major` - Major version bump - `version:minor` - Minor version bump - `version:patch` - Patch version bump (default for main) @@ -257,6 +262,7 @@ git push && git push --tags ``` The release script will: + - Check for uncommitted changes - Bump the version in package.json - Update iOS and Android native versions @@ -310,7 +316,9 @@ bundle exec fastlane ios build_local ### Troubleshooting Deployments #### Version Already Exists + The build system auto-increments build numbers. If you get version conflicts: + ```bash # Check current versions node scripts/version.cjs status @@ -321,6 +329,7 @@ node scripts/version.cjs bump-build android ``` #### Certificate Issues (iOS) + ```bash # Check certificate validity bundle exec fastlane ios check_certs @@ -332,18 +341,22 @@ bundle exec fastlane ios check_certs ``` #### Play Store Upload Issues + If automated upload fails, the AAB is saved locally: + - Location: `android/app/build/outputs/bundle/release/app-release.aab` - Upload manually via Play Console ### Build Optimization The CI/CD pipeline uses extensive caching: + - **iOS builds**: ~15 minutes (with cache) - **Android builds**: ~10 minutes (with cache) - **First build**: ~25 minutes (no cache) To speed up local builds: + ```bash # Clean only what's necessary yarn clean:build # Clean build artifacts only diff --git a/app/package.json b/app/package.json index a5347efe2..4a7d99631 100644 --- a/app/package.json +++ b/app/package.json @@ -24,8 +24,8 @@ "clean:xcode": "rm -rf ~/Library/Developer/Xcode/DerivedData", "clean:xcode-env-local": "rm -f ios/.xcode.env.local", "find:type-imports": "node scripts/find-type-import-issues.mjs", - "fmt": "prettier --check .", - "fmt:fix": "prettier --write .", + "fmt": "yarn prettier --check .", + "fmt:fix": "yarn prettier --write .", "format": "yarn nice", "ia": "yarn install-app", "imports:fix": "node ./scripts/alias-imports.cjs", diff --git a/app/src/components/NavBar/HomeNavBar.tsx b/app/src/components/NavBar/HomeNavBar.tsx index 057189e76..0adc5030f 100644 --- a/app/src/components/NavBar/HomeNavBar.tsx +++ b/app/src/components/NavBar/HomeNavBar.tsx @@ -10,12 +10,12 @@ import type { NativeStackHeaderProps } from '@react-navigation/native-stack'; import { Clipboard as ClipboardIcon } from '@tamagui/lucide-icons'; import type { SelfApp } from '@selfxyz/common/utils/appType'; +import { useSelfAppStore } from '@selfxyz/mobile-sdk-alpha/stores'; import { NavBar } from '@/components/NavBar/BaseNavBar'; import ActivityIcon from '@/images/icons/activity.svg'; import ScanIcon from '@/images/icons/qr_scan.svg'; import SettingsIcon from '@/images/icons/settings.svg'; -import { useSelfAppStore } from '@/stores/selfAppStore'; import { black, charcoal, neutral400, slate50, white } from '@/utils/colors'; import { extraYPadding } from '@/utils/constants'; import { buttonTap } from '@/utils/haptic'; diff --git a/app/src/screens/prove/ConfirmBelongingScreen.tsx b/app/src/screens/prove/ConfirmBelongingScreen.tsx index 68b56f62f..360d64736 100644 --- a/app/src/screens/prove/ConfirmBelongingScreen.tsx +++ b/app/src/screens/prove/ConfirmBelongingScreen.tsx @@ -60,13 +60,13 @@ const ConfirmBelongingScreen: React.FC = () => { if (permissionGranted) { const token = await getFCMToken(); if (token) { - setFcmToken(token); + setFcmToken(token, selfClient); trackEvent(ProofEvents.FCM_TOKEN_STORED); } } // Mark as user confirmed - proving will start automatically when ready - setUserConfirmed(); + setUserConfirmed(selfClient); // Navigate to loading screen navigate(); diff --git a/app/src/screens/prove/ProofRequestStatusScreen.tsx b/app/src/screens/prove/ProofRequestStatusScreen.tsx index df9f91e81..c1e6819b8 100644 --- a/app/src/screens/prove/ProofRequestStatusScreen.tsx +++ b/app/src/screens/prove/ProofRequestStatusScreen.tsx @@ -11,6 +11,7 @@ import { useIsFocused } from '@react-navigation/native'; import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; +import { useSelfAppStore } from '@selfxyz/mobile-sdk-alpha/stores'; import loadingAnimation from '@/assets/animations/loading/misc.json'; import failAnimation from '@/assets/animations/proof_failed.json'; @@ -24,7 +25,6 @@ import useHapticNavigation from '@/hooks/useHapticNavigation'; import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; import { ProofStatus } from '@/stores/proof-types'; import { useProofHistoryStore } from '@/stores/proofHistoryStore'; -import { useSelfAppStore } from '@/stores/selfAppStore'; import { black, white } from '@/utils/colors'; import { buttonTap, diff --git a/app/src/screens/prove/ProveScreen.tsx b/app/src/screens/prove/ProveScreen.tsx index 1d505f98d..c2e9d06b1 100644 --- a/app/src/screens/prove/ProveScreen.tsx +++ b/app/src/screens/prove/ProveScreen.tsx @@ -24,6 +24,7 @@ import type { SelfAppDisclosureConfig } from '@selfxyz/common/utils/appType'; import { formatEndpoint } from '@selfxyz/common/utils/scope'; import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; +import { useSelfAppStore } from '@selfxyz/mobile-sdk-alpha/stores'; import miscAnimation from '@/assets/animations/loading/misc.json'; import { HeldPrimaryButtonProveScreen } from '@/components/buttons/HeldPrimaryButtonProveScreen'; @@ -37,7 +38,6 @@ import { } from '@/providers/passportDataProvider'; import { ProofStatus } from '@/stores/proof-types'; import { useProofHistoryStore } from '@/stores/proofHistoryStore'; -import { useSelfAppStore } from '@/stores/selfAppStore'; import { black, slate300, white } from '@/utils/colors'; import { formatUserId } from '@/utils/formatUserId'; import { buttonTap } from '@/utils/haptic'; @@ -149,7 +149,7 @@ const ProveScreen: React.FC = () => { ); function onVerify() { - provingStore.setUserConfirmed(); + provingStore.setUserConfirmed(selfClient); buttonTap(); trackEvent(ProofEvents.PROOF_VERIFY_CONFIRMATION_ACCEPTED, { appName: selectedApp?.appName, diff --git a/app/src/screens/prove/QRCodeViewFinderScreen.tsx b/app/src/screens/prove/QRCodeViewFinderScreen.tsx index 766ae2de8..5f9168ed4 100644 --- a/app/src/screens/prove/QRCodeViewFinderScreen.tsx +++ b/app/src/screens/prove/QRCodeViewFinderScreen.tsx @@ -14,6 +14,7 @@ import { import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; +import { useSelfAppStore } from '@selfxyz/mobile-sdk-alpha/stores'; import qrScanAnimation from '@/assets/animations/qr_scan.json'; import { SecondaryButton } from '@/components/buttons/SecondaryButton'; @@ -26,7 +27,6 @@ import useConnectionModal from '@/hooks/useConnectionModal'; import useHapticNavigation from '@/hooks/useHapticNavigation'; import QRScan from '@/images/icons/qr_code.svg'; import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; -import { useSelfAppStore } from '@/stores/selfAppStore'; import { black, slate800, white } from '@/utils/colors'; import { parseAndValidateUrlParams } from '@/utils/deeplinks'; diff --git a/app/src/utils/deeplinks.ts b/app/src/utils/deeplinks.ts index 79e70a775..1ded995d2 100644 --- a/app/src/utils/deeplinks.ts +++ b/app/src/utils/deeplinks.ts @@ -7,9 +7,9 @@ import { Linking, Platform } from 'react-native'; import { countries } from '@selfxyz/common/constants/countries'; import type { IdDocInput } from '@selfxyz/common/utils'; +import { useSelfAppStore } from '@selfxyz/mobile-sdk-alpha/stores'; import { navigationRef } from '@/navigation'; -import { useSelfAppStore } from '@/stores/selfAppStore'; import useUserStore from '@/stores/userStore'; // Validation patterns for each expected parameter diff --git a/app/src/utils/proving/provingInputs.ts b/app/src/utils/proving/provingInputs.ts deleted file mode 100644 index 028a8561a..000000000 --- a/app/src/utils/proving/provingInputs.ts +++ /dev/null @@ -1,31 +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 type { DocumentCategory, PassportData } from '@selfxyz/common/types'; -import type { SelfApp } from '@selfxyz/common/utils'; -import { generateTEEInputsDiscloseStateless } from '@selfxyz/common/utils/circuits/registerInputs'; -import { useProtocolStore } from '@selfxyz/mobile-sdk-alpha/stores'; - -export function generateTEEInputsDisclose( - secret: string, - passportData: PassportData, - selfApp: SelfApp, -) { - return generateTEEInputsDiscloseStateless( - secret, - passportData, - selfApp, - (document: DocumentCategory, tree) => { - const protocolStore = useProtocolStore.getState(); - switch (tree) { - case 'ofac': - return protocolStore[document].ofac_trees; - case 'commitment': - return protocolStore[document].commitment_tree; - default: - throw new Error('Unknown tree type'); - } - }, - ); -} diff --git a/app/src/utils/proving/provingMachine.ts b/app/src/utils/proving/provingMachine.ts index f7df25b51..463678f22 100644 --- a/app/src/utils/proving/provingMachine.ts +++ b/app/src/utils/proving/provingMachine.ts @@ -38,6 +38,7 @@ import { } from '@selfxyz/common/utils/proving'; import { clearPassportData, + generateTEEInputsDisclose, hasAnyValidRegisteredDocument, loadSelectedDocument, markCurrentDocumentAsRegistered, @@ -49,13 +50,10 @@ import { PassportEvents, ProofEvents, } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; -import { useProtocolStore } from '@selfxyz/mobile-sdk-alpha/stores'; - -import { useSelfAppStore } from '@/stores/selfAppStore'; -import analytics from '@/utils/analytics'; -import { generateTEEInputsDisclose } from '@/utils/proving/provingInputs'; - -const { trackEvent } = analytics(); +import { + useProtocolStore, + useSelfAppStore, +} from '@selfxyz/mobile-sdk-alpha/stores'; export type ProvingStateType = // Initial states @@ -163,12 +161,20 @@ export const getPostVerificationRoute = () => { // return cloudBackupEnabled ? 'AccountVerifiedSuccess' : 'SaveRecoveryPhrase'; }; +type WsHandlers = { + message: (event: MessageEvent) => void; + open: () => void; + error: (error: Event) => void; + close: (event: CloseEvent) => void; +}; + interface ProvingState { currentState: ProvingStateType; attestation: number[] | null; serverPublicKey: string | null; sharedKey: Buffer | null; wsConnection: WebSocket | null; + wsHandlers: WsHandlers | null; socketConnection: Socket | null; uuid: string | null; userConfirmed: boolean; @@ -180,30 +186,43 @@ interface ProvingState { endpointType: EndpointType | null; fcmToken: string | null; env: 'prod' | 'stg' | null; - selfClient: SelfClient | null; - setFcmToken: (token: string) => void; + setFcmToken: (token: string, selfClient: SelfClient) => void; init: ( selfClient: SelfClient, circuitType: 'dsc' | 'disclose' | 'register', userConfirmed?: boolean, ) => Promise; - startFetchingData: () => Promise; + startFetchingData: (selfClient: SelfClient) => Promise; validatingDocument: (selfClient: SelfClient) => Promise; - initTeeConnection: () => Promise; - startProving: () => Promise; + initTeeConnection: (selfClient: SelfClient) => Promise; + startProving: (selfClient: SelfClient) => Promise; postProving: (selfClient: SelfClient) => void; - setUserConfirmed: () => void; - _closeConnections: () => void; - _generatePayload: () => Promise; - _handleWebSocketMessage: (event: MessageEvent) => Promise; + setUserConfirmed: (selfClient: SelfClient) => void; + _closeConnections: (selfClient: SelfClient) => void; + _generatePayload: (selfClient: SelfClient) => Promise<{ + jsonrpc: '2.0'; + method: 'openpassport_submit_request'; + id: 2; + params: { + uuid: string | null; + nonce: number[]; + cipher_text: number[]; + auth_tag: number[]; + }; + }>; + _handleWebSocketMessage: ( + event: MessageEvent, + selfClient: SelfClient, + ) => Promise; _handleRegisterErrorOrFailure: (selfClient: SelfClient) => void; _startSocketIOStatusListener: ( receivedUuid: string, endpointType: EndpointType, + selfClient: SelfClient, ) => void; - _handleWsOpen: () => void; - _handleWsError: (error: Event) => void; - _handleWsClose: (event: CloseEvent) => void; + _handleWsOpen: (selfClient: SelfClient) => void; + _handleWsError: (error: Event, selfClient: SelfClient) => void; + _handleWsClose: (event: CloseEvent, selfClient: SelfClient) => void; _handlePassportNotSupported: (selfClient: SelfClient) => void; _handleAccountRecoveryChoice: (selfClient: SelfClient) => void; @@ -220,22 +239,24 @@ export const useProvingStore = create((set, get) => { ) { newActor.subscribe((state: StateFrom) => { console.log(`State transition: ${state.value}`); - trackEvent(ProofEvents.PROVING_STATE_CHANGE, { state: state.value }); + selfClient.trackEvent(ProofEvents.PROVING_STATE_CHANGE, { + state: state.value, + }); set({ currentState: state.value as ProvingStateType }); if (state.value === 'fetching_data') { - get().startFetchingData(); + get().startFetchingData(selfClient); } if (state.value === 'validating_document') { get().validatingDocument(selfClient); } if (state.value === 'init_tee_connexion') { - get().initTeeConnection(); + get().initTeeConnection(selfClient); } if (state.value === 'ready_to_prove' && get().userConfirmed) { - get().startProving(); + get().startProving(selfClient); } if (state.value === 'post_proving') { @@ -250,7 +271,7 @@ export const useProvingStore = create((set, get) => { } if (state.value === 'completed') { - trackEvent(ProofEvents.PROOF_COMPLETED, { + selfClient.trackEvent(ProofEvents.PROOF_COMPLETED, { circuitType: get().circuitType, }); @@ -313,6 +334,7 @@ export const useProvingStore = create((set, get) => { serverPublicKey: null, sharedKey: null, wsConnection: null, + wsHandlers: null, socketConnection: null, uuid: null, userConfirmed: false, @@ -325,21 +347,22 @@ export const useProvingStore = create((set, get) => { reason: null, endpointType: null, fcmToken: null, - selfClient: null, - setFcmToken: (token: string) => { + setFcmToken: (token: string, selfClient: SelfClient) => { set({ fcmToken: token }); - trackEvent(ProofEvents.FCM_TOKEN_STORED); + selfClient.trackEvent(ProofEvents.FCM_TOKEN_STORED); }, - _handleWebSocketMessage: async (event: MessageEvent) => { + _handleWebSocketMessage: async ( + event: MessageEvent, + selfClient: SelfClient, + ) => { if (!actor) { console.error('Cannot process message: State machine not initialized.'); return; } - try { const result = JSON.parse(event.data); if (result.result?.attestation) { - trackEvent(ProofEvents.ATTESTATION_RECEIVED); + selfClient?.trackEvent(ProofEvents.ATTESTATION_RECEIVED); const attestationData = result.result.attestation; set({ attestation: attestationData }); @@ -352,7 +375,7 @@ export const useProvingStore = create((set, get) => { return; } - trackEvent(ProofEvents.ATTESTATION_VERIFIED); + selfClient?.trackEvent(ProofEvents.ATTESTATION_VERIFIED); const serverKey = ec.keyFromPublic(serverPubkey as string, 'hex'); const derivedKey = clientKey.derive(serverKey.getPublic()); @@ -361,7 +384,7 @@ export const useProvingStore = create((set, get) => { serverPublicKey: serverPubkey, sharedKey: Buffer.from(derivedKey.toArray('be', 32)), }); - trackEvent(ProofEvents.SHARED_KEY_DERIVED); + selfClient?.trackEvent(ProofEvents.SHARED_KEY_DERIVED); actor!.send({ type: 'CONNECT_SUCCESS' }); } else if ( @@ -369,7 +392,7 @@ export const useProvingStore = create((set, get) => { typeof result.result === 'string' && !result.error ) { - trackEvent(ProofEvents.WS_HELLO_ACK); + selfClient?.trackEvent(ProofEvents.WS_HELLO_ACK); // Received status from TEE const statusUuid = result.result; if (get().uuid !== statusUuid) { @@ -384,18 +407,24 @@ export const useProvingStore = create((set, get) => { console.error( 'Cannot start Socket.IO listener: endpointType not set.', ); - trackEvent(ProofEvents.PROOF_FAILED, { + selfClient?.trackEvent(ProofEvents.PROOF_FAILED, { circuitType: get().circuitType, error: get().error_code ?? 'unknown', }); actor!.send({ type: 'PROVE_ERROR' }); return; } - get()._startSocketIOStatusListener(statusUuid, endpointType); + get()._startSocketIOStatusListener( + statusUuid, + endpointType, + selfClient, + ); } else if (result.error) { console.error('Received error from TEE:', result.error); - trackEvent(ProofEvents.TEE_WS_ERROR, { error: result.error }); - trackEvent(ProofEvents.PROOF_FAILED, { + selfClient?.trackEvent(ProofEvents.TEE_WS_ERROR, { + error: result.error, + }); + selfClient?.trackEvent(ProofEvents.PROOF_FAILED, { circuitType: get().circuitType, error: get().error_code ?? 'unknown', }); @@ -406,15 +435,15 @@ export const useProvingStore = create((set, get) => { } catch (error) { console.error('Error processing WebSocket message:', error); if (get().currentState === 'init_tee_connexion') { - trackEvent(ProofEvents.TEE_CONN_FAILED, { + selfClient?.trackEvent(ProofEvents.TEE_CONN_FAILED, { message: error instanceof Error ? error.message : String(error), }); actor!.send({ type: 'CONNECT_ERROR' }); } else { - trackEvent(ProofEvents.TEE_WS_ERROR, { + selfClient?.trackEvent(ProofEvents.TEE_WS_ERROR, { error: error instanceof Error ? error.message : String(error), }); - trackEvent(ProofEvents.PROOF_FAILED, { + selfClient?.trackEvent(ProofEvents.PROOF_FAILED, { circuitType: get().circuitType, error: get().error_code ?? 'unknown', }); @@ -440,6 +469,7 @@ export const useProvingStore = create((set, get) => { _startSocketIOStatusListener: ( receivedUuid: string, endpointType: EndpointType, + selfClient: SelfClient, ) => { if (!actor) { console.error('Cannot start Socket.IO listener: Actor not available.'); @@ -452,19 +482,19 @@ export const useProvingStore = create((set, get) => { transports: ['websocket'], }); set({ socketConnection: socket }); - trackEvent(ProofEvents.SOCKETIO_CONN_STARTED); + selfClient.trackEvent(ProofEvents.SOCKETIO_CONN_STARTED); socket.on('connect', () => { socket?.emit('subscribe', receivedUuid); - trackEvent(ProofEvents.SOCKETIO_SUBSCRIBED); + selfClient.trackEvent(ProofEvents.SOCKETIO_SUBSCRIBED); }); socket.on('connect_error', error => { console.error('SocketIO connection error:', error); - trackEvent(ProofEvents.SOCKETIO_CONNECT_ERROR, { + selfClient.trackEvent(ProofEvents.SOCKETIO_CONNECT_ERROR, { message: error instanceof Error ? error.message : String(error), }); - trackEvent(ProofEvents.PROOF_FAILED, { + selfClient.trackEvent(ProofEvents.PROOF_FAILED, { circuitType: get().circuitType, error: get().error_code ?? 'unknown', }); @@ -479,8 +509,8 @@ export const useProvingStore = create((set, get) => { console.error( 'SocketIO disconnected unexpectedly during proof listening.', ); - trackEvent(ProofEvents.SOCKETIO_DISCONNECT_UNEXPECTED); - trackEvent(ProofEvents.PROOF_FAILED, { + selfClient.trackEvent(ProofEvents.SOCKETIO_DISCONNECT_UNEXPECTED); + selfClient.trackEvent(ProofEvents.PROOF_FAILED, { circuitType: get().circuitType, error: get().error_code ?? 'unknown', }); @@ -492,7 +522,7 @@ export const useProvingStore = create((set, get) => { socket.on('status', (message: unknown) => { const data = typeof message === 'string' ? JSON.parse(message) : message; - trackEvent(ProofEvents.SOCKETIO_STATUS_RECEIVED, { + selfClient.trackEvent(ProofEvents.SOCKETIO_STATUS_RECEIVED, { status: data.status, }); if (data.status === 3 || data.status === 5) { @@ -501,11 +531,11 @@ export const useProvingStore = create((set, get) => { ); console.error(data); set({ error_code: data.error_code, reason: data.reason }); - trackEvent(ProofEvents.SOCKETIO_PROOF_FAILURE, { + selfClient.trackEvent(ProofEvents.SOCKETIO_PROOF_FAILURE, { error_code: data.error_code, reason: data.reason, }); - trackEvent(ProofEvents.PROOF_FAILED, { + selfClient.trackEvent(ProofEvents.PROOF_FAILED, { circuitType: get().circuitType, error: data.error_code ?? 'unknown', }); @@ -516,15 +546,15 @@ export const useProvingStore = create((set, get) => { socket?.disconnect(); set({ socketConnection: null }); if (get().circuitType === 'register') { - trackEvent(ProofEvents.REGISTER_COMPLETED); + selfClient.trackEvent(ProofEvents.REGISTER_COMPLETED); } - trackEvent(ProofEvents.SOCKETIO_PROOF_SUCCESS); + selfClient.trackEvent(ProofEvents.SOCKETIO_PROOF_SUCCESS); actor!.send({ type: 'PROVE_SUCCESS' }); } }); }, - _handleWsOpen: () => { + _handleWsOpen: (selfClient: SelfClient) => { if (!actor) { return; } @@ -534,7 +564,7 @@ export const useProvingStore = create((set, get) => { } const connectionUuid = v4(); - trackEvent(ProofEvents.CONNECTION_UUID_GENERATED, { + selfClient.trackEvent(ProofEvents.CONNECTION_UUID_GENERATED, { connection_uuid: connectionUuid, }); @@ -551,11 +581,11 @@ export const useProvingStore = create((set, get) => { uuid: connectionUuid, }, }; - trackEvent(ProofEvents.WS_HELLO_SENT); + selfClient.trackEvent(ProofEvents.WS_HELLO_SENT); ws.send(JSON.stringify(helloBody)); }, - _handleWsError: (error: Event) => { + _handleWsError: (error: Event, selfClient: SelfClient) => { console.error('TEE WebSocket error event:', error); if (!actor) { return; @@ -564,11 +594,12 @@ export const useProvingStore = create((set, get) => { new MessageEvent('error', { data: JSON.stringify({ error: 'WebSocket connection error' }), }), + selfClient, ); }, - _handleWsClose: (event: CloseEvent) => { - trackEvent(ProofEvents.TEE_WS_CLOSED, { + _handleWsClose: (event: CloseEvent, selfClient: SelfClient) => { + selfClient.trackEvent(ProofEvents.TEE_WS_CLOSED, { code: event.code, reason: event.reason, }); @@ -588,6 +619,7 @@ export const useProvingStore = create((set, get) => { new MessageEvent('error', { data: JSON.stringify({ error: 'WebSocket closed unexpectedly' }), }), + selfClient, ); } if (get().wsConnection) { @@ -600,8 +632,8 @@ export const useProvingStore = create((set, get) => { circuitType: 'dsc' | 'disclose' | 'register', userConfirmed: boolean = false, ) => { - trackEvent(ProofEvents.PROVING_INIT); - get()._closeConnections(); + selfClient.trackEvent(ProofEvents.PROVING_INIT); + get()._closeConnections(selfClient); if (actor) { try { @@ -624,28 +656,29 @@ export const useProvingStore = create((set, get) => { circuitType, endpointType: null, env: null, - selfClient, }); actor = createActor(provingMachine); setupActorSubscriptions(actor, selfClient); actor.start(); - trackEvent(ProofEvents.DOCUMENT_LOAD_STARTED); + selfClient.trackEvent(ProofEvents.DOCUMENT_LOAD_STARTED); const selectedDocument = await loadSelectedDocument(selfClient); if (!selectedDocument) { console.error('No document found for proving'); - trackEvent(PassportEvents.PASSPORT_DATA_NOT_FOUND, { stage: 'init' }); + selfClient.trackEvent(PassportEvents.PASSPORT_DATA_NOT_FOUND, { + stage: 'init', + }); actor!.send({ type: 'PASSPORT_DATA_NOT_FOUND' }); return; } const { data: passportData } = selectedDocument; - const secret = await get().selfClient?.getPrivateKey(); + const secret = await selfClient.getPrivateKey(); if (!secret) { console.error('Could not load secret'); - trackEvent(ProofEvents.LOAD_SECRET_FAILED); + selfClient.trackEvent(ProofEvents.LOAD_SECRET_FAILED); actor!.send({ type: 'ERROR' }); return; } @@ -656,12 +689,12 @@ export const useProvingStore = create((set, get) => { set({ passportData, secret, env }); set({ circuitType }); actor.send({ type: 'FETCH_DATA' }); - trackEvent(ProofEvents.FETCH_DATA_STARTED); + selfClient.trackEvent(ProofEvents.FETCH_DATA_STARTED); }, - startFetchingData: async () => { + startFetchingData: async (selfClient: SelfClient) => { _checkActorInitialized(actor); - trackEvent(ProofEvents.FETCH_DATA_STARTED); + selfClient.trackEvent(ProofEvents.FETCH_DATA_STARTED); try { const { passportData, env } = get(); if (!passportData) { @@ -669,7 +702,7 @@ export const useProvingStore = create((set, get) => { } if (!passportData?.dsc_parsed) { console.error('Missing parsed DSC in passport data'); - trackEvent(ProofEvents.FETCH_DATA_FAILED, { + selfClient.trackEvent(ProofEvents.FETCH_DATA_FAILED, { message: 'Missing parsed DSC in passport data', }); actor!.send({ type: 'FETCH_ERROR' }); @@ -681,11 +714,11 @@ export const useProvingStore = create((set, get) => { [ document ].fetch_all(env!, (passportData as PassportData).dsc_parsed!.authorityKeyIdentifier); - trackEvent(ProofEvents.FETCH_DATA_SUCCESS); + selfClient.trackEvent(ProofEvents.FETCH_DATA_SUCCESS); actor!.send({ type: 'FETCH_SUCCESS' }); } catch (error) { console.error('Error fetching data:', error); - trackEvent(ProofEvents.FETCH_DATA_FAILED, { + selfClient.trackEvent(ProofEvents.FETCH_DATA_FAILED, { message: error instanceof Error ? error.message : String(error), }); actor!.send({ type: 'FETCH_ERROR' }); @@ -695,7 +728,7 @@ export const useProvingStore = create((set, get) => { validatingDocument: async (selfClient: SelfClient) => { _checkActorInitialized(actor); // TODO: for the disclosure, we could check that the selfApp is a valid one. - trackEvent(ProofEvents.VALIDATION_STARTED); + selfClient.trackEvent(ProofEvents.VALIDATION_STARTED); try { const { passportData, secret, circuitType } = get(); if (!passportData) { @@ -711,7 +744,7 @@ export const useProvingStore = create((set, get) => { isSupported.status, isSupported.details, ); - trackEvent(PassportEvents.UNSUPPORTED_PASSPORT, { + selfClient.trackEvent(PassportEvents.UNSUPPORTED_PASSPORT, { status: isSupported.status, details: isSupported.details, }); @@ -732,7 +765,7 @@ export const useProvingStore = create((set, get) => { getCommitmentTree, ); if (isRegisteredWithLocalCSCA) { - trackEvent(ProofEvents.VALIDATION_SUCCESS); + selfClient.trackEvent(ProofEvents.VALIDATION_SUCCESS); actor!.send({ type: 'VALIDATION_SUCCESS' }); return; } else { @@ -770,7 +803,7 @@ export const useProvingStore = create((set, get) => { } })(); - trackEvent(ProofEvents.ALREADY_REGISTERED); + selfClient.trackEvent(ProofEvents.ALREADY_REGISTERED); actor!.send({ type: 'ALREADY_REGISTERED' }); return; } @@ -779,7 +812,7 @@ export const useProvingStore = create((set, get) => { console.warn( 'Passport is nullified, but not registered with this secret. Navigating to AccountRecoveryChoice', ); - trackEvent(ProofEvents.PASSPORT_NULLIFIER_ONCHAIN); + selfClient.trackEvent(ProofEvents.PASSPORT_NULLIFIER_ONCHAIN); actor!.send({ type: 'ACCOUNT_RECOVERY_CHOICE' }); return; } @@ -789,22 +822,22 @@ export const useProvingStore = create((set, get) => { useProtocolStore.getState()[document].dsc_tree, ); if (isDscRegistered) { - trackEvent(ProofEvents.DSC_IN_TREE); + selfClient.trackEvent(ProofEvents.DSC_IN_TREE); set({ circuitType: 'register' }); } - trackEvent(ProofEvents.VALIDATION_SUCCESS); + selfClient.trackEvent(ProofEvents.VALIDATION_SUCCESS); actor!.send({ type: 'VALIDATION_SUCCESS' }); } } catch (error) { console.error('Error validating passport:', error); - trackEvent(ProofEvents.VALIDATION_FAILED, { + selfClient.trackEvent(ProofEvents.VALIDATION_FAILED, { message: error instanceof Error ? error.message : String(error), }); actor!.send({ type: 'VALIDATION_ERROR' }); } }, - initTeeConnection: async (): Promise => { + initTeeConnection: async (selfClient: SelfClient): Promise => { const { passportData } = get(); if (!passportData) { throw new Error('PassportData is not available'); @@ -850,26 +883,36 @@ export const useProvingStore = create((set, get) => { throw new Error('No WebSocket URL available for TEE connection'); } - get()._closeConnections(); - trackEvent(ProofEvents.TEE_CONN_STARTED); + get()._closeConnections(selfClient); + selfClient.trackEvent(ProofEvents.TEE_CONN_STARTED); return new Promise(resolve => { const ws = new WebSocket(wsRpcUrl); - set({ wsConnection: ws }); const handleConnectSuccess = () => { - trackEvent(ProofEvents.TEE_CONN_SUCCESS); + selfClient.trackEvent(ProofEvents.TEE_CONN_SUCCESS); resolve(true); }; const handleConnectError = (msg: string = 'connect_error') => { - trackEvent(ProofEvents.TEE_CONN_FAILED, { message: msg }); + selfClient.trackEvent(ProofEvents.TEE_CONN_FAILED, { message: msg }); resolve(false); }; - ws.addEventListener('message', get()._handleWebSocketMessage); - ws.addEventListener('open', get()._handleWsOpen); - ws.addEventListener('error', get()._handleWsError); - ws.addEventListener('close', get()._handleWsClose); + // Create stable handler functions + const wsHandlers: WsHandlers = { + message: (event: MessageEvent) => + get()._handleWebSocketMessage(event, selfClient), + open: () => get()._handleWsOpen(selfClient), + error: (error: Event) => get()._handleWsError(error, selfClient), + close: (event: CloseEvent) => get()._handleWsClose(event, selfClient), + }; + + set({ wsConnection: ws, wsHandlers }); + + ws.addEventListener('message', wsHandlers.message); + ws.addEventListener('open', wsHandlers.open); + ws.addEventListener('error', wsHandlers.error); + ws.addEventListener('close', wsHandlers.close); if (!actor) { return; @@ -886,7 +929,7 @@ export const useProvingStore = create((set, get) => { }); }, - startProving: async () => { + startProving: async (selfClient: SelfClient) => { _checkActorInitialized(actor); const { wsConnection, sharedKey, passportData, secret, uuid, fcmToken } = get(); @@ -899,7 +942,7 @@ export const useProvingStore = create((set, get) => { console.error( 'Cannot start proving: Missing wsConnection, sharedKey, passportData, secret, or uuid.', ); - trackEvent(ProofEvents.PROOF_FAILED, { + selfClient.trackEvent(ProofEvents.PROOF_FAILED, { circuitType: get().circuitType, error: get().error_code ?? 'unknown', }); @@ -915,27 +958,27 @@ export const useProvingStore = create((set, get) => { registerDeviceToken, } = require('@/utils/notifications/notificationService'); const isMockPassport = passportData?.mock; - trackEvent(ProofEvents.DEVICE_TOKEN_REG_STARTED); + selfClient.trackEvent(ProofEvents.DEVICE_TOKEN_REG_STARTED); await registerDeviceToken(uuid, fcmToken, isMockPassport); - trackEvent(ProofEvents.DEVICE_TOKEN_REG_SUCCESS); + selfClient.trackEvent(ProofEvents.DEVICE_TOKEN_REG_SUCCESS); } catch (error) { console.error('Error registering device token:', error); - trackEvent(ProofEvents.DEVICE_TOKEN_REG_FAILED, { + selfClient.trackEvent(ProofEvents.DEVICE_TOKEN_REG_FAILED, { message: error instanceof Error ? error.message : String(error), }); // Continue with the proving process even if token registration fails } } - trackEvent(ProofEvents.PAYLOAD_GEN_STARTED); - const submitBody = await get()._generatePayload(); + selfClient.trackEvent(ProofEvents.PAYLOAD_GEN_STARTED); + const submitBody = await get()._generatePayload(selfClient); wsConnection.send(JSON.stringify(submitBody)); - trackEvent(ProofEvents.PAYLOAD_SENT); - trackEvent(ProofEvents.PROVING_PROCESS_STARTED); + selfClient.trackEvent(ProofEvents.PAYLOAD_SENT); + selfClient.trackEvent(ProofEvents.PROVING_PROCESS_STARTED); actor!.send({ type: 'START_PROVING' }); } catch (error) { console.error('Error during startProving preparation/send:', error); - trackEvent(ProofEvents.PROOF_FAILED, { + selfClient.trackEvent(ProofEvents.PROOF_FAILED, { circuitType: get().circuitType, error: get().error_code ?? 'unknown', }); @@ -943,43 +986,43 @@ export const useProvingStore = create((set, get) => { } }, - setUserConfirmed: () => { + setUserConfirmed: (selfClient: SelfClient) => { set({ userConfirmed: true }); - trackEvent(ProofEvents.USER_CONFIRMED); + selfClient.trackEvent(ProofEvents.USER_CONFIRMED); if (get().currentState === 'ready_to_prove') { - get().startProving(); + get().startProving(selfClient); } }, postProving: (selfClient: SelfClient) => { _checkActorInitialized(actor); const { circuitType } = get(); - trackEvent(ProofEvents.POST_PROVING_STARTED); + selfClient.trackEvent(ProofEvents.POST_PROVING_STARTED); if (circuitType === 'dsc') { setTimeout(() => { - trackEvent(ProofEvents.POST_PROVING_CHAIN_STEP, { + selfClient.trackEvent(ProofEvents.POST_PROVING_CHAIN_STEP, { from: 'dsc', to: 'register', }); get().init(selfClient, 'register', true); }, 1500); } else if (circuitType === 'register') { - trackEvent(ProofEvents.POST_PROVING_COMPLETED); + selfClient.trackEvent(ProofEvents.POST_PROVING_COMPLETED); actor!.send({ type: 'COMPLETED' }); } else if (circuitType === 'disclose') { - trackEvent(ProofEvents.POST_PROVING_COMPLETED); + selfClient.trackEvent(ProofEvents.POST_PROVING_COMPLETED); actor!.send({ type: 'COMPLETED' }); } }, - _closeConnections: () => { - const ws = get().wsConnection; - if (ws) { + _closeConnections: (selfClient: SelfClient) => { + const { wsConnection: ws, wsHandlers } = get(); + if (ws && wsHandlers) { try { - ws.removeEventListener('message', get()._handleWebSocketMessage); - ws.removeEventListener('open', get()._handleWsOpen); - ws.removeEventListener('error', get()._handleWsError); - ws.removeEventListener('close', get()._handleWsClose); + ws.removeEventListener('message', wsHandlers.message); + ws.removeEventListener('open', wsHandlers.open); + ws.removeEventListener('error', wsHandlers.error); + ws.removeEventListener('close', wsHandlers.close); ws.close(); } catch (error) { console.error( @@ -987,7 +1030,7 @@ export const useProvingStore = create((set, get) => { error, ); } - set({ wsConnection: null }); + set({ wsConnection: null, wsHandlers: null }); } const socket = get().socketConnection; @@ -1004,7 +1047,7 @@ export const useProvingStore = create((set, get) => { }); }, - _generatePayload: async () => { + _generatePayload: async (selfClient: SelfClient) => { const { circuitType, passportData, secret, uuid, sharedKey, env } = get(); if (!passportData) { throw new Error('PassportData is not available'); @@ -1082,8 +1125,8 @@ export const useProvingStore = create((set, get) => { forgeKey, ); - trackEvent(ProofEvents.PAYLOAD_GEN_COMPLETED); - trackEvent(ProofEvents.PAYLOAD_ENCRYPTED); + selfClient.trackEvent(ProofEvents.PAYLOAD_GEN_COMPLETED); + selfClient.trackEvent(ProofEvents.PAYLOAD_ENCRYPTED); // Persist endpointType for later Socket.IO connection set({ endpointType: endpointType as EndpointType }); diff --git a/app/tests/utils/deeplinks.test.ts b/app/tests/utils/deeplinks.test.ts index b40b58d28..23334bb68 100644 --- a/app/tests/utils/deeplinks.test.ts +++ b/app/tests/utils/deeplinks.test.ts @@ -13,7 +13,7 @@ jest.mock('@/navigation', () => ({ })); const mockSelfAppStore = { useSelfAppStore: { getState: jest.fn() } }; -jest.mock('@/stores/selfAppStore', () => mockSelfAppStore); +jest.mock('@selfxyz/mobile-sdk-alpha/stores', () => mockSelfAppStore); const mockUserStore = { default: { getState: jest.fn() } }; jest.mock('@/stores/userStore', () => ({ diff --git a/app/tests/utils/proving/provingMachine.generatePayload.test.ts b/app/tests/utils/proving/provingMachine.generatePayload.test.ts index cd0d52f62..c11084e7c 100644 --- a/app/tests/utils/proving/provingMachine.generatePayload.test.ts +++ b/app/tests/utils/proving/provingMachine.generatePayload.test.ts @@ -2,9 +2,12 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import { useProtocolStore } from '@selfxyz/mobile-sdk-alpha/stores'; +import type { SelfClient } from '@selfxyz/mobile-sdk-alpha'; +import { + useProtocolStore, + useSelfAppStore, +} from '@selfxyz/mobile-sdk-alpha/stores'; -import { useSelfAppStore } from '@/stores/selfAppStore'; import { useProvingStore } from '@/utils/proving/provingMachine'; jest.mock('xstate', () => { @@ -17,32 +20,6 @@ jest.mock('@/utils/analytics', () => () => ({ trackEvent: jest.fn(), })); -// Mock the proving inputs to return predictable data -jest.mock('@/utils/proving/provingInputs', () => ({ - generateTEEInputsDisclose: jest.fn(() => ({ - inputs: { s: 1 }, - circuitName: 'vc_and_disclose', - endpointType: 'https', - endpoint: 'https://dis', - })), -})); - -// Mock the common register/dsc inputs where provingMachine actually imports from -jest.mock('@selfxyz/common/utils/circuits/registerInputs', () => ({ - generateTEEInputsRegister: jest.fn(() => ({ - inputs: { r: 1 }, - circuitName: 'reg', - endpointType: 'celo', - endpoint: 'https://reg', - })), - generateTEEInputsDSC: jest.fn(() => ({ - inputs: { d: 1 }, - circuitName: 'dsc', - endpointType: 'celo', - endpoint: 'https://dsc', - })), -})); - // Mock the proving utils jest.mock('@selfxyz/common/utils/proving', () => { const actual = jest.requireActual('@selfxyz/common/utils/proving') as any; @@ -54,6 +31,52 @@ jest.mock('@selfxyz/common/utils/proving', () => { cipher_text: [1], auth_tag: [2], })), + generateTEEInputsRegister: jest.fn(() => ({ + inputs: { r: 1 }, + circuitName: 'reg', + endpointType: 'celo', + endpoint: 'https://reg', + })), + generateTEEInputsDSC: jest.fn(() => ({ + inputs: { d: 1 }, + circuitName: 'dsc', + endpointType: 'celo', + endpoint: 'https://dsc', + })), + generateTEEInputsDisclose: jest.fn(() => ({ + inputs: { s: 1 }, + circuitName: 'vc_and_disclose', + endpointType: 'https', + endpoint: 'https://dis', + })), + }; +}); + +// Mock the proving utils +jest.mock('@selfxyz/common/utils/circuits/registerInputs', () => { + const actual = jest.requireActual( + '@selfxyz/common/utils/circuits/registerInputs', + ) as any; + return { + ...actual, + generateTEEInputsRegister: jest.fn(() => ({ + inputs: { r: 1 }, + circuitName: 'reg', + endpointType: 'celo', + endpoint: 'https://reg', + })), + generateTEEInputsDSC: jest.fn(() => ({ + inputs: { d: 1 }, + circuitName: 'dsc', + endpointType: 'celo', + endpoint: 'https://dsc', + })), + generateTEEInputsDiscloseStateless: jest.fn(() => ({ + inputs: { s: 1 }, + circuitName: 'vc_and_disclose', + endpointType: 'https', + endpoint: 'https://dis', + })), }; }); @@ -89,13 +112,17 @@ const { getPayload, encryptAES256GCM, } = require('@selfxyz/common/utils/proving'); -const { generateTEEInputsDisclose } = require('@/utils/proving/provingInputs'); + const { generateTEEInputsRegister, generateTEEInputsDSC, + generateTEEInputsDiscloseStateless, } = require('@selfxyz/common/utils/circuits/registerInputs'); describe('_generatePayload', () => { + const selfClient: SelfClient = { + trackEvent: jest.fn(), + } as unknown as SelfClient; beforeEach(() => { jest.clearAllMocks(); useProvingStore.setState({ @@ -203,7 +230,9 @@ describe('_generatePayload', () => { it('register circuit', async () => { useProvingStore.setState({ circuitType: 'register' }); - const payload = await useProvingStore.getState()._generatePayload(); + const payload = await useProvingStore + .getState() + ._generatePayload(selfClient); expect(generateTEEInputsRegister).toHaveBeenCalled(); expect(getPayload).toHaveBeenCalled(); expect(encryptAES256GCM).toHaveBeenCalled(); @@ -218,7 +247,9 @@ describe('_generatePayload', () => { it('dsc circuit', async () => { useProvingStore.setState({ circuitType: 'dsc' }); - const payload = await useProvingStore.getState()._generatePayload(); + const payload = await useProvingStore + .getState() + ._generatePayload(selfClient); expect(generateTEEInputsDSC).toHaveBeenCalled(); expect(useProvingStore.getState().endpointType).toBe('celo'); expect(payload.params.uuid).toBe('123'); @@ -226,8 +257,10 @@ describe('_generatePayload', () => { it('disclose circuit', async () => { useProvingStore.setState({ circuitType: 'disclose' }); - const payload = await useProvingStore.getState()._generatePayload(); - expect(generateTEEInputsDisclose).toHaveBeenCalled(); + const payload = await useProvingStore + .getState() + ._generatePayload(selfClient); + expect(generateTEEInputsDiscloseStateless).toHaveBeenCalled(); expect(useProvingStore.getState().endpointType).toBe('https'); expect(payload.params.uuid).toBe('123'); }); diff --git a/app/tests/utils/proving/provingMachine.startFetchingData.test.ts b/app/tests/utils/proving/provingMachine.startFetchingData.test.ts index 159456b9a..6dd4c4dac 100644 --- a/app/tests/utils/proving/provingMachine.startFetchingData.test.ts +++ b/app/tests/utils/proving/provingMachine.startFetchingData.test.ts @@ -55,6 +55,7 @@ jest.mock('@selfxyz/mobile-sdk-alpha', () => { }); describe('startFetchingData', () => { + let mockSelfClient: SelfClient; beforeEach(async () => { jest.clearAllMocks(); const { @@ -71,8 +72,9 @@ describe('startFetchingData', () => { unsafe_getPrivateKey.mockResolvedValue('secret'); // Create mock selfClient - const mockSelfClient = { + mockSelfClient = { getPrivateKey: jest.fn().mockResolvedValue('mock-secret'), + trackEvent: jest.fn(), } as unknown as SelfClient; useProtocolStore.setState({ @@ -91,7 +93,7 @@ describe('startFetchingData', () => { }); it('emits FETCH_ERROR when dsc_parsed is missing', async () => { - await useProvingStore.getState().startFetchingData(); + await useProvingStore.getState().startFetchingData(mockSelfClient); expect( useProtocolStore.getState().passport.fetch_all, diff --git a/app/tests/utils/proving/provingMachine.test.ts b/app/tests/utils/proving/provingMachine.test.ts index 4c38c03bd..76afb7023 100644 --- a/app/tests/utils/proving/provingMachine.test.ts +++ b/app/tests/utils/proving/provingMachine.test.ts @@ -36,8 +36,8 @@ describe('provingMachine registration completion', () => { useProvingStore(state => state.init), ); const emitMock = jest.fn(); - const selfClient = { + trackEvent: jest.fn(), emit: emitMock, } as unknown as SelfClient; diff --git a/app/vite.config.ts b/app/vite.config.ts index 73f4b6ec1..243279557 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -166,7 +166,6 @@ export default defineConfig({ ], 'screens-prove-utils': [ './src/utils/proving/index.ts', - './src/utils/proving/provingInputs.ts', './src/utils/proving/loadingScreenStateText.ts', ], diff --git a/common/index.ts b/common/index.ts index e24d5620b..add454242 100644 --- a/common/index.ts +++ b/common/index.ts @@ -55,10 +55,14 @@ export { SelfAppBuilder, bigIntToString, brutforceSignatureAlgorithmDsc, + buildSMT, + calculateUserIdentifierHash, findStartPubKeyIndex, formatEndpoint, formatMrz, genAndInitMockPassportData, + genMockIdDoc, + genMockIdDocAndInitDataParsing, generateCircuitInputsDSC, generateCircuitInputsRegister, generateCircuitInputsVCandDisclose, @@ -69,28 +73,17 @@ export { getLeafCscaTree, getLeafDscTree, getSKIPEM, + getSolidityPackedUserContextData, getUniversalLink, hashEndpointWithScope, initElliptic, initPassportDataParsing, parseCertificateSimple, parseDscCertificateData, - genMockIdDoc, - genMockIdDocAndInitDataParsing, - buildSMT, - calculateUserIdentifierHash, - getSolidityPackedUserContextData, stringToBigInt, } from './src/utils/index.js'; -export { - prepareAadhaarRegisterTestData, - prepareAadhaarDiscloseTestData, - prepareAadhaarRegisterData, -} from './src/utils/aadhaar/mockData.js'; -export { generateTestData, testCustomData } from './src/utils/aadhaar/utils.js'; export { createSelector } from './src/utils/aadhaar/constants.js'; - // Hash utilities export { customHasher, @@ -99,3 +92,11 @@ export { hash, packBytesAndPoseidon, } from './src/utils/hash.js'; + +export { generateTestData, testCustomData } from './src/utils/aadhaar/utils.js'; + +export { + prepareAadhaarDiscloseTestData, + prepareAadhaarRegisterData, + prepareAadhaarRegisterTestData, +} from './src/utils/aadhaar/mockData.js'; diff --git a/common/package.json b/common/package.json index e5c5ac99b..01dc5c8c0 100644 --- a/common/package.json +++ b/common/package.json @@ -647,8 +647,8 @@ "build:watch": "tsup --watch", "format": "prettier --write .", "lint": "prettier --check .", - "lint:imports": "eslint . --fix", - "lint:imports:check": "eslint .", + "lint:imports": "yarn eslint --fix .", + "lint:imports:check": "yarn eslint .", "nice": "yarn format && yarn lint:imports", "nice:check": "yarn lint && yarn lint:imports:check", "prepublishOnly": "yarn build", diff --git a/common/src/constants/constants.ts b/common/src/constants/constants.ts index 34483cabb..ae6a93d93 100644 --- a/common/src/constants/constants.ts +++ b/common/src/constants/constants.ts @@ -1,7 +1,9 @@ export type Country3LetterCode = keyof typeof countryCodes; export type document_type = 'passport' | 'id_card'; export type hashAlgosTypes = 'sha512' | 'sha384' | 'sha256' | 'sha224' | 'sha1'; +export const AADHAAR_ATTESTATION_ID = '3'; export const API_URL = 'https://api.self.xyz'; + export const API_URL_STAGING = 'https://api.staging.self.xyz'; export const CHAIN_NAME = 'celo'; @@ -42,8 +44,6 @@ export const CSCA_TREE_URL_STAGING = 'https://tree.staging.self.xyz/csca'; export const CSCA_TREE_URL_STAGING_ID_CARD = 'https://tree.staging.self.xyz/csca-id'; -export const AADHAAR_ATTESTATION_ID = '3'; - // we make it global here because passing it to generateCircuitInputsRegister caused trouble export const DEFAULT_MAJORITY = '18'; diff --git a/common/src/utils/aadhaar/build_aadhaar_ofac_smt.ts b/common/src/utils/aadhaar/build_aadhaar_ofac_smt.ts index 9681943d9..6722939fd 100644 --- a/common/src/utils/aadhaar/build_aadhaar_ofac_smt.ts +++ b/common/src/utils/aadhaar/build_aadhaar_ofac_smt.ts @@ -1,4 +1,5 @@ import * as fs from 'fs'; + import { buildAadhaarSMT } from '../trees.js'; async function build_aadhaar_ofac_smt() { diff --git a/common/src/utils/aadhaar/constants.ts b/common/src/utils/aadhaar/constants.ts index 5a7d7b07a..cb3d1cbde 100644 --- a/common/src/utils/aadhaar/constants.ts +++ b/common/src/utils/aadhaar/constants.ts @@ -1,3 +1,4 @@ +/* eslint-disable sort-exports/sort-exports */ export const MAX_FIELD_BYTE_SIZE = 31; export const NAME_MAX_LENGTH = 2 * MAX_FIELD_BYTE_SIZE; // 62 bytes export const TOTAL_REVEAL_DATA_LENGTH = 119; diff --git a/common/src/utils/aadhaar/mockData.ts b/common/src/utils/aadhaar/mockData.ts index b438a5dbc..5ba5eff5e 100644 --- a/common/src/utils/aadhaar/mockData.ts +++ b/common/src/utils/aadhaar/mockData.ts @@ -1,29 +1,34 @@ -import { calculateAge, generateTestData, testCustomData } from './utils.js'; -import { - convertBigIntToByteArray, - decompressByteArray, - splitToWords, - extractPhoto, -} from '@anon-aadhaar/core'; -import { bufferToHex, Uint8ArrayToCharArray } from '@zk-email/helpers/dist/binary-format.js'; -import { sha256Pad } from '@zk-email/helpers/dist/sha-utils.js'; -import { testQRData } from './assets/dataInput.js'; -import { stringToAsciiArray } from './utils.js'; -import { packBytesAndPoseidon } from '../hash.js'; -import { poseidon5 } from 'poseidon-lite'; import forge from 'node-forge'; -import { LeanIMT } from '@openpassport/zk-kit-lean-imt'; -import { SMT } from '@openpassport/zk-kit-smt'; +import { poseidon5 } from 'poseidon-lite'; + +import { COMMITMENT_TREE_DEPTH } from '../../constants/constants.js'; import { findIndexInTree, formatInput } from '../circuits/generateInputs.js'; +import { packBytesAndPoseidon } from '../hash.js'; import { generateMerkleProof, generateSMTProof, getNameDobLeafAadhaar, getNameYobLeafAahaar, } from '../trees.js'; +import { testQRData } from './assets/dataInput.js'; +import { + calculateAge, + extractQRDataFields, + generateTestData, + stringToAsciiArray, + testCustomData, +} from './utils.js'; -import { COMMITMENT_TREE_DEPTH } from '../../constants/constants.js'; -import { extractQRDataFields } from './utils.js'; +import { + convertBigIntToByteArray, + decompressByteArray, + extractPhoto, + splitToWords, +} from '@anon-aadhaar/core'; +import { LeanIMT } from '@openpassport/zk-kit-lean-imt'; +import { SMT } from '@openpassport/zk-kit-smt'; +import { bufferToHex, Uint8ArrayToCharArray } from '@zk-email/helpers/dist/binary-format.js'; +import { sha256Pad } from '@zk-email/helpers/dist/sha-utils.js'; // Helper function to compute padded name function computePaddedName(name: string): number[] { @@ -163,19 +168,25 @@ function processQRDataSimple(qrData: string) { }; } -export function prepareAadhaarRegisterTestData( - privKeyPem: string, - pubkeyPem: string, +export function prepareAadhaarDiscloseTestData( + privateKeyPem: string, + merkletree: LeanIMT, + nameAndDob_smt: SMT, + nameAndYob_smt: SMT, + scope: string, secret: string, + user_identifier: string, + selector: string, name?: string, dateOfBirth?: string, gender?: string, pincode?: string, state?: string, - timestamp?: string + timestamp?: string, + updateTree?: boolean ) { const sharedData = processQRData( - privKeyPem, + privateKeyPem, name, dateOfBirth, gender, @@ -184,36 +195,14 @@ export function prepareAadhaarRegisterTestData( timestamp ); - const delimiterIndices: number[] = []; - for (let i = 0; i < sharedData.qrDataPadded.length; i++) { - if (sharedData.qrDataPadded[i] === 255) { - delimiterIndices.push(i); - } - if (delimiterIndices.length === 18) { - break; - } - } - let photoEOI = 0; - for (let i = delimiterIndices[17]; i < sharedData.qrDataPadded.length - 1; i++) { - if (sharedData.qrDataPadded[i + 1] === 217 && sharedData.qrDataPadded[i] === 255) { - photoEOI = i + 1; - } - } - if (photoEOI === 0) { - throw new Error('Photo EOI not found'); - } - - const signatureBytes = sharedData.decodedData.slice( - sharedData.decodedData.length - 256, - sharedData.decodedData.length + const { age, currentYear, currentMonth, currentDay } = calculateAge( + sharedData.extractedFields.dob, + sharedData.extractedFields.mob, + sharedData.extractedFields.yob ); - const signature = BigInt('0x' + bufferToHex(Buffer.from(signatureBytes)).toString()); - - const publicKey = forge.pki.publicKeyFromPem(pubkeyPem); - - const modulusHex = publicKey.n.toString(16); - const pubKey = BigInt('0x' + modulusHex); + const uppercaseName = computeUppercasePaddedName(sharedData.extractedFields.name); + const genderAscii = stringToAsciiArray(sharedData.extractedFields.gender)[0]; const nullifier = nullifierHash(sharedData.extractedFields); const packedCommitment = computePackedCommitment(sharedData.extractedFields); const commitment = computeCommitment( @@ -224,14 +213,74 @@ export function prepareAadhaarRegisterTestData( BigInt(sharedData.photoHash) ); + const paddedName = computePaddedName(sharedData.extractedFields.name); + + if (updateTree) { + merkletree.insert(BigInt(commitment)); + } + + const index = findIndexInTree(merkletree, BigInt(commitment)); + const { + siblings, + path: merkle_path, + leaf_depth, + } = generateMerkleProof(merkletree, index, COMMITMENT_TREE_DEPTH); + + const namedob_leaf = getNameDobLeafAadhaar( + sharedData.extractedFields.name, + sharedData.extractedFields.yob, + sharedData.extractedFields.mob, + sharedData.extractedFields.dob + ); + const nameyob_leaf = getNameYobLeafAahaar( + sharedData.extractedFields.name, + sharedData.extractedFields.yob + ); + + const { + root: ofac_name_dob_smt_root, + closestleaf: ofac_name_dob_smt_leaf_key, + siblings: ofac_name_dob_smt_siblings, + } = generateSMTProof(nameAndDob_smt, namedob_leaf); + + const { + root: ofac_name_yob_smt_root, + closestleaf: ofac_name_yob_smt_leaf_key, + siblings: ofac_name_yob_smt_siblings, + } = generateSMTProof(nameAndYob_smt, nameyob_leaf); + const inputs = { - qrDataPadded: Uint8ArrayToCharArray(sharedData.qrDataPadded), - qrDataPaddedLength: sharedData.qrDataPaddedLen, - delimiterIndices: delimiterIndices, - signature: splitToWords(signature, BigInt(121), BigInt(17)), - pubKey: splitToWords(pubKey, BigInt(121), BigInt(17)), + attestation_id: '3', secret: secret, - photoEOI: photoEOI, + qrDataHash: BigInt(sharedData.qrHash).toString(), + gender: genderAscii.toString(), + yob: stringToAsciiArray(sharedData.extractedFields.yob), + mob: stringToAsciiArray(sharedData.extractedFields.mob), + dob: stringToAsciiArray(sharedData.extractedFields.dob), + name: formatInput(paddedName), + aadhaar_last_4digits: stringToAsciiArray(sharedData.extractedFields.aadhaarLast4Digits), + pincode: stringToAsciiArray(sharedData.extractedFields.pincode), + state: stringToAsciiArray(sharedData.extractedFields.state.padEnd(31, '\0')), + ph_no_last_4digits: stringToAsciiArray(sharedData.extractedFields.phoneNoLast4Digits), + photoHash: formatInput(BigInt(sharedData.photoHash)), + merkle_root: formatInput(BigInt(merkletree.root)), + leaf_depth: formatInput(leaf_depth), + path: formatInput(merkle_path), + siblings: formatInput(siblings), + ofac_name_dob_smt_leaf_key: formatInput(BigInt(ofac_name_dob_smt_leaf_key)), + ofac_name_dob_smt_root: formatInput(BigInt(ofac_name_dob_smt_root)), + ofac_name_dob_smt_siblings: formatInput(ofac_name_dob_smt_siblings), + ofac_name_yob_smt_leaf_key: formatInput(BigInt(ofac_name_yob_smt_leaf_key)), + ofac_name_yob_smt_root: formatInput(BigInt(ofac_name_yob_smt_root)), + ofac_name_yob_smt_siblings: formatInput(ofac_name_yob_smt_siblings), + selector, + minimumAge: formatInput(age - 2), + currentYear: formatInput(currentYear), + currentMonth: formatInput(currentMonth), + currentDay: formatInput(currentDay), + scope: formatInput(BigInt(scope)), + user_identifier: formatInput(BigInt(user_identifier)), + forbidden_countries_list: [...Array(120)].map((x) => '0'), }; return { @@ -323,25 +372,19 @@ export async function prepareAadhaarRegisterData(qrData: string, secret: string, }; } -export function prepareAadhaarDiscloseTestData( - privateKeyPem: string, - merkletree: LeanIMT, - nameAndDob_smt: SMT, - nameAndYob_smt: SMT, - scope: string, +export function prepareAadhaarRegisterTestData( + privKeyPem: string, + pubkeyPem: string, secret: string, - user_identifier: string, - selector: string, name?: string, dateOfBirth?: string, gender?: string, pincode?: string, state?: string, - timestamp?: string, - updateTree?: boolean + timestamp?: string ) { const sharedData = processQRData( - privateKeyPem, + privKeyPem, name, dateOfBirth, gender, @@ -350,14 +393,36 @@ export function prepareAadhaarDiscloseTestData( timestamp ); - const { age, currentYear, currentMonth, currentDay } = calculateAge( - sharedData.extractedFields.dob, - sharedData.extractedFields.mob, - sharedData.extractedFields.yob + const delimiterIndices: number[] = []; + for (let i = 0; i < sharedData.qrDataPadded.length; i++) { + if (sharedData.qrDataPadded[i] === 255) { + delimiterIndices.push(i); + } + if (delimiterIndices.length === 18) { + break; + } + } + let photoEOI = 0; + for (let i = delimiterIndices[17]; i < sharedData.qrDataPadded.length - 1; i++) { + if (sharedData.qrDataPadded[i + 1] === 217 && sharedData.qrDataPadded[i] === 255) { + photoEOI = i + 1; + } + } + if (photoEOI === 0) { + throw new Error('Photo EOI not found'); + } + + const signatureBytes = sharedData.decodedData.slice( + sharedData.decodedData.length - 256, + sharedData.decodedData.length ); + const signature = BigInt('0x' + bufferToHex(Buffer.from(signatureBytes)).toString()); + + const publicKey = forge.pki.publicKeyFromPem(pubkeyPem); + + const modulusHex = publicKey.n.toString(16); + const pubKey = BigInt('0x' + modulusHex); - const uppercaseName = computeUppercasePaddedName(sharedData.extractedFields.name); - const genderAscii = stringToAsciiArray(sharedData.extractedFields.gender)[0]; const nullifier = nullifierHash(sharedData.extractedFields); const packedCommitment = computePackedCommitment(sharedData.extractedFields); const commitment = computeCommitment( @@ -368,74 +433,14 @@ export function prepareAadhaarDiscloseTestData( BigInt(sharedData.photoHash) ); - const paddedName = computePaddedName(sharedData.extractedFields.name); - - if (updateTree) { - merkletree.insert(BigInt(commitment)); - } - - const index = findIndexInTree(merkletree, BigInt(commitment)); - const { - siblings, - path: merkle_path, - leaf_depth, - } = generateMerkleProof(merkletree, index, COMMITMENT_TREE_DEPTH); - - const namedob_leaf = getNameDobLeafAadhaar( - sharedData.extractedFields.name, - sharedData.extractedFields.yob, - sharedData.extractedFields.mob, - sharedData.extractedFields.dob - ); - const nameyob_leaf = getNameYobLeafAahaar( - sharedData.extractedFields.name, - sharedData.extractedFields.yob - ); - - const { - root: ofac_name_dob_smt_root, - closestleaf: ofac_name_dob_smt_leaf_key, - siblings: ofac_name_dob_smt_siblings, - } = generateSMTProof(nameAndDob_smt, namedob_leaf); - - const { - root: ofac_name_yob_smt_root, - closestleaf: ofac_name_yob_smt_leaf_key, - siblings: ofac_name_yob_smt_siblings, - } = generateSMTProof(nameAndYob_smt, nameyob_leaf); - const inputs = { - attestation_id: '3', + qrDataPadded: Uint8ArrayToCharArray(sharedData.qrDataPadded), + qrDataPaddedLength: sharedData.qrDataPaddedLen, + delimiterIndices: delimiterIndices, + signature: splitToWords(signature, BigInt(121), BigInt(17)), + pubKey: splitToWords(pubKey, BigInt(121), BigInt(17)), secret: secret, - qrDataHash: BigInt(sharedData.qrHash).toString(), - gender: genderAscii.toString(), - yob: stringToAsciiArray(sharedData.extractedFields.yob), - mob: stringToAsciiArray(sharedData.extractedFields.mob), - dob: stringToAsciiArray(sharedData.extractedFields.dob), - name: formatInput(paddedName), - aadhaar_last_4digits: stringToAsciiArray(sharedData.extractedFields.aadhaarLast4Digits), - pincode: stringToAsciiArray(sharedData.extractedFields.pincode), - state: stringToAsciiArray(sharedData.extractedFields.state.padEnd(31, '\0')), - ph_no_last_4digits: stringToAsciiArray(sharedData.extractedFields.phoneNoLast4Digits), - photoHash: formatInput(BigInt(sharedData.photoHash)), - merkle_root: formatInput(BigInt(merkletree.root)), - leaf_depth: formatInput(leaf_depth), - path: formatInput(merkle_path), - siblings: formatInput(siblings), - ofac_name_dob_smt_leaf_key: formatInput(BigInt(ofac_name_dob_smt_leaf_key)), - ofac_name_dob_smt_root: formatInput(BigInt(ofac_name_dob_smt_root)), - ofac_name_dob_smt_siblings: formatInput(ofac_name_dob_smt_siblings), - ofac_name_yob_smt_leaf_key: formatInput(BigInt(ofac_name_yob_smt_leaf_key)), - ofac_name_yob_smt_root: formatInput(BigInt(ofac_name_yob_smt_root)), - ofac_name_yob_smt_siblings: formatInput(ofac_name_yob_smt_siblings), - selector, - minimumAge: formatInput(age - 2), - currentYear: formatInput(currentYear), - currentMonth: formatInput(currentMonth), - currentDay: formatInput(currentDay), - scope: formatInput(BigInt(scope)), - user_identifier: formatInput(BigInt(user_identifier)), - forbidden_countries_list: [...Array(120)].map((x) => '0'), + photoEOI: photoEOI, }; return { diff --git a/common/src/utils/aadhaar/utils.ts b/common/src/utils/aadhaar/utils.ts index f8284a453..772a450ac 100644 --- a/common/src/utils/aadhaar/utils.ts +++ b/common/src/utils/aadhaar/utils.ts @@ -1,27 +1,40 @@ +import forge from 'node-forge'; + import { convertBigIntToByteArray, decompressByteArray, - returnFullId, - rawDataToCompressedQR, - replaceBytesBetween, - IdFields, extractPhoto, - getRandomBytes, getEndIndex, + getRandomBytes, + IdFields, + rawDataToCompressedQR, + replaceBytesBetween, + returnFullId, } from '@anon-aadhaar/core'; -import forge from 'node-forge'; -export function stringToAsciiArray(str: string) { - return str.split('').map((char) => char.charCodeAt(0)); +export interface ExtractedQRData { + name: string; + yob: string; + mob: string; + dob: string; + gender: string; + pincode: string; + state: string; + aadhaarLast4Digits: string; + phoneNoLast4Digits: string; + timestamp: string; } -// This is the official test data issued by the UIDAI -// In this script we'll change the signed data to emulate the specs of the Aadhaar QR V2 -// and sign the data again with our own certificates. -// data on https://uidai.gov.in/en/ecosystem/authentication-devices-documents/qr-code-reader.html -// This data is copied from https://github.dev/anon-aadhaar/anon-aadhaar/blob/main/packages/circuits/src/helpers/extractor.circom -export const testCustomData = - '2374971804270526477833002468783965837992554564899874087591661303561346432389832047870524302186901344489362368642972767716416349990805756094923115719687656090691368051627957878187788907419297818953295185555346288172578594637886352753543271000481717080003254556962148594350559820352806251787713278744047402230989238559317351232114240089849934148895256488140236015024800731753594740948640957680138566468247224859669467819596919398964809164399637893729212452791889199675715949918925838319591794702333094022248132120531152523331442741730158840977243402215102904932650832502847295644794421419704633765033761284508863534321317394686768650111457751139630853448637215423705157211510636160227953566227527799608082928846103264491539001327407775670834868948113753614112563650255058316849200536533335903554984254814901522086937767458409075617572843449110393213525925388131214952874629655799772119820372255291052673056372346072235458198199995637720424196884145247220163810790179386390283738429482893152518286247124911446073389185062482901364671389605727763080854673156754021728522287806275420847159574631844674460263574901590412679291518508010087116598357407343835408554094619585212373168435612645646129147973594416508676872819776522537778717985070402222824965034768103900739105784663244748432502180989441389718131079445941981681118258324511923246198334046020123727749408128519721102477302359413240175102907322619462289965085963377744024233678337951462006962521823224880199210318367946130004264196899778609815012001799773327514133268825910089483612283510244566484854597156100473055413090101948456959122378865704840756793122956663218517626099291311352417342899623681483097817511136427210593032393600010728324905512596767095096153856032112835755780472808814199620390836980020899858288860556611564167406292139646289142056168261133256777093245980048335918156712295254776487472431445495668303900536289283098315798552328294391152828182614909451410115516297083658174657554955228963550255866282688308751041517464999930825273776417639569977754844191402927594739069037851707477839207593911886893016618794870530622356073909077832279869798641545167528509966656120623184120128052588408742941658045827255866966100249857968956536613250770326334844204927432961924987891433020671754710428050564671868464658436926086493709176888821257183419013229795869757265111599482263223604228286513011751601176504567030118257385997460972803240338899836840030438830725520798480181575861397469056536579877274090338750406459700907704031830137890544492015701251066934352867527112361743047684237105216779177819594030160887368311805926405114938744235859610328064947158936962470654636736991567663705830950312548447653861922078087824048793236971354828540758657075837209006713701763902429652486225300535997260665898927924843608750347193892239342462507130025307878412116604096773706728162016134101751551184021079984480254041743057914746472840768175369369852937574401874295943063507273467384747124843744395375119899278823903202010381949145094804675442110869084589592876721655764753871572233276245590041302887094585204427900634246823674277680009401177473636685542700515621164233992970974893989913447733956146698563285998205950467321954304'; +export const FIELD_POSITIONS = { + REFERENCE_ID: 2, + NAME: 3, + DOB: 4, + GENDER: 5, + PINCODE: 11, + STATE: 13, + PHONE_NO: 17, + PHOTO: 18, +} as const; // Will sign the data with the keys generated for test const signNewTestData = (newSignedData: Uint8Array, privKeyPem: string) => { @@ -45,65 +58,33 @@ const signNewTestData = (newSignedData: Uint8Array, privKeyPem: string) => { } }; -export const generateTestData = ({ - privKeyPem, - data, - dob, - gender, - pincode, - state, - photo, - name, - timestamp, -}: { - privKeyPem: string; - data: string; - dob?: string; - gender?: string; - pincode?: string; - state?: string; - photo?: boolean; - name?: string; - timestamp?: string; -}) => { - const qrDataBytes = convertBigIntToByteArray(BigInt(data)); - const decodedData = decompressByteArray(qrDataBytes); - - // Turning test data V1 into V2 - // Adding the version specifier prefix, - // the last 4 digits of phone number and timestamp to now - const dataToSign = createCustomV2TestData({ - signedData: decodedData.slice(0, decodedData.length - 256), - dob, - pincode, - gender, - state, - photo, - name, - timestamp, - }); +export function calculateAge( + dob: string, + mob: string, + yob: string +): { age: number; currentYear: number; currentMonth: number; currentDay: number } { + const currentDate = new Date(); + const currentYear = currentDate.getFullYear(); + const currentMonth = currentDate.getMonth() + 1; // getMonth() returns 0-11 + const currentDay = currentDate.getDate(); - // Signing the newly generated testData - const signature = signNewTestData(dataToSign, privKeyPem); + const birthYear = parseInt(yob); + const birthMonth = parseInt(mob); + const birthDay = parseInt(dob); - // Reconstructing the whole QR data - const tempData = Buffer.concat([dataToSign, signature]); + let age = currentYear - birthYear; - // Compressing the data to have it in the same format as the QR code - const newCompressedData = rawDataToCompressedQR(tempData); - const newQrData = { - testQRData: newCompressedData.toString(), - ...returnFullId(dataToSign), + if (currentMonth < birthMonth || (currentMonth === birthMonth && currentDay < birthDay)) { + age--; + } + return { + age, + currentYear, + currentMonth, + currentDay, }; +} - return newQrData; -}; - -// This modify the test data to make it compliant with the secure Aadhaar QR V2 2022 -// - Adds the version specifier at the beginning 'V2' -// - Mocks last 4 digits of phone number '1234' after VTC -// - Refresh timestamp data to now -// - Optionally it can take parameters to change the test data fields (dob, pinCode, gender, state) export const createCustomV2TestData = ({ signedData, dob, @@ -238,99 +219,6 @@ export const createCustomV2TestData = ({ return newData; }; -export function calculateAge( - dob: string, - mob: string, - yob: string -): { age: number; currentYear: number; currentMonth: number; currentDay: number } { - const currentDate = new Date(); - const currentYear = currentDate.getFullYear(); - const currentMonth = currentDate.getMonth() + 1; // getMonth() returns 0-11 - const currentDay = currentDate.getDate(); - - const birthYear = parseInt(yob); - const birthMonth = parseInt(mob); - const birthDay = parseInt(dob); - - let age = currentYear - birthYear; - - if (currentMonth < birthMonth || (currentMonth === birthMonth && currentDay < birthDay)) { - age--; - } - return { - age, - currentYear, - currentMonth, - currentDay, - }; -} - -export function returnNewDateString(timestamp?: string): string { - const newDate = timestamp ? new Date(+timestamp) : new Date(); - - // Convert the UTC date to IST by adding 5 hours and 30 minutes - const offsetHours = 5; - const offsetMinutes = 30; - newDate.setUTCHours(newDate.getUTCHours() + offsetHours); - newDate.setUTCMinutes(newDate.getUTCMinutes() + offsetMinutes); - - return ( - newDate.getUTCFullYear().toString() + - (newDate.getUTCMonth() + 1).toString().padStart(2, '0') + - newDate.getUTCDate().toString().padStart(2, '0') + - newDate.getUTCHours().toString().padStart(2, '0') + - newDate.getUTCMinutes().toString().padStart(2, '0') + - newDate.getUTCSeconds().toString().padStart(2, '0') + - newDate.getUTCMilliseconds().toString().padStart(3, '0') - ); -} -export const FIELD_POSITIONS = { - REFERENCE_ID: 2, - NAME: 3, - DOB: 4, - GENDER: 5, - PINCODE: 11, - STATE: 13, - PHONE_NO: 17, - PHOTO: 18, -} as const; - -function asciiArrayToString(asciiArray: number[]): string { - return asciiArray - .filter((byte) => byte !== 0) - .map((byte) => String.fromCharCode(byte)) - .join(''); -} - -function extractFieldData( - data: Uint8Array, - delimiterIndices: number[], - position: number -): number[] { - const startIndex = delimiterIndices[position - 1] + 1; - const endIndex = delimiterIndices[position]; - - const fieldData: number[] = []; - for (let i = startIndex; i < endIndex; i++) { - fieldData.push(data[i]); - } - - return fieldData; -} - -export interface ExtractedQRData { - name: string; - yob: string; - mob: string; - dob: string; - gender: string; - pincode: string; - state: string; - aadhaarLast4Digits: string; - phoneNoLast4Digits: string; - timestamp: string; -} - export function extractQRDataFields(qrData: string | Uint8Array): ExtractedQRData { let qrDataBytes: Uint8Array; @@ -428,3 +316,117 @@ export function extractQRDataFields(qrData: string | Uint8Array): ExtractedQRDat timestamp, }; } + +export const generateTestData = ({ + privKeyPem, + data, + dob, + gender, + pincode, + state, + photo, + name, + timestamp, +}: { + privKeyPem: string; + data: string; + dob?: string; + gender?: string; + pincode?: string; + state?: string; + photo?: boolean; + name?: string; + timestamp?: string; +}) => { + const qrDataBytes = convertBigIntToByteArray(BigInt(data)); + const decodedData = decompressByteArray(qrDataBytes); + + // Turning test data V1 into V2 + // Adding the version specifier prefix, + // the last 4 digits of phone number and timestamp to now + const dataToSign = createCustomV2TestData({ + signedData: decodedData.slice(0, decodedData.length - 256), + dob, + pincode, + gender, + state, + photo, + name, + timestamp, + }); + + // Signing the newly generated testData + const signature = signNewTestData(dataToSign, privKeyPem); + + // Reconstructing the whole QR data + const tempData = Buffer.concat([dataToSign, signature]); + + // Compressing the data to have it in the same format as the QR code + const newCompressedData = rawDataToCompressedQR(tempData); + const newQrData = { + testQRData: newCompressedData.toString(), + ...returnFullId(dataToSign), + }; + + return newQrData; +}; + +export function returnNewDateString(timestamp?: string): string { + const newDate = timestamp ? new Date(+timestamp) : new Date(); + + // Convert the UTC date to IST by adding 5 hours and 30 minutes + const offsetHours = 5; + const offsetMinutes = 30; + newDate.setUTCHours(newDate.getUTCHours() + offsetHours); + newDate.setUTCMinutes(newDate.getUTCMinutes() + offsetMinutes); + + return ( + newDate.getUTCFullYear().toString() + + (newDate.getUTCMonth() + 1).toString().padStart(2, '0') + + newDate.getUTCDate().toString().padStart(2, '0') + + newDate.getUTCHours().toString().padStart(2, '0') + + newDate.getUTCMinutes().toString().padStart(2, '0') + + newDate.getUTCSeconds().toString().padStart(2, '0') + + newDate.getUTCMilliseconds().toString().padStart(3, '0') + ); +} + +function asciiArrayToString(asciiArray: number[]): string { + return asciiArray + .filter((byte) => byte !== 0) + .map((byte) => String.fromCharCode(byte)) + .join(''); +} + +function extractFieldData( + data: Uint8Array, + delimiterIndices: number[], + position: number +): number[] { + const startIndex = delimiterIndices[position - 1] + 1; + const endIndex = delimiterIndices[position]; + + const fieldData: number[] = []; + for (let i = startIndex; i < endIndex; i++) { + fieldData.push(data[i]); + } + + return fieldData; +} + +// This is the official test data issued by the UIDAI +// In this script we'll change the signed data to emulate the specs of the Aadhaar QR V2 +// and sign the data again with our own certificates. +// data on https://uidai.gov.in/en/ecosystem/authentication-devices-documents/qr-code-reader.html +// This data is copied from https://github.dev/anon-aadhaar/anon-aadhaar/blob/main/packages/circuits/src/helpers/extractor.circom +export function stringToAsciiArray(str: string) { + return str.split('').map((char) => char.charCodeAt(0)); +} + +// This modify the test data to make it compliant with the secure Aadhaar QR V2 2022 +// - Adds the version specifier at the beginning 'V2' +// - Mocks last 4 digits of phone number '1234' after VTC +// - Refresh timestamp data to now +// - Optionally it can take parameters to change the test data fields (dob, pinCode, gender, state) +export const testCustomData = + '2374971804270526477833002468783965837992554564899874087591661303561346432389832047870524302186901344489362368642972767716416349990805756094923115719687656090691368051627957878187788907419297818953295185555346288172578594637886352753543271000481717080003254556962148594350559820352806251787713278744047402230989238559317351232114240089849934148895256488140236015024800731753594740948640957680138566468247224859669467819596919398964809164399637893729212452791889199675715949918925838319591794702333094022248132120531152523331442741730158840977243402215102904932650832502847295644794421419704633765033761284508863534321317394686768650111457751139630853448637215423705157211510636160227953566227527799608082928846103264491539001327407775670834868948113753614112563650255058316849200536533335903554984254814901522086937767458409075617572843449110393213525925388131214952874629655799772119820372255291052673056372346072235458198199995637720424196884145247220163810790179386390283738429482893152518286247124911446073389185062482901364671389605727763080854673156754021728522287806275420847159574631844674460263574901590412679291518508010087116598357407343835408554094619585212373168435612645646129147973594416508676872819776522537778717985070402222824965034768103900739105784663244748432502180989441389718131079445941981681118258324511923246198334046020123727749408128519721102477302359413240175102907322619462289965085963377744024233678337951462006962521823224880199210318367946130004264196899778609815012001799773327514133268825910089483612283510244566484854597156100473055413090101948456959122378865704840756793122956663218517626099291311352417342899623681483097817511136427210593032393600010728324905512596767095096153856032112835755780472808814199620390836980020899858288860556611564167406292139646289142056168261133256777093245980048335918156712295254776487472431445495668303900536289283098315798552328294391152828182614909451410115516297083658174657554955228963550255866282688308751041517464999930825273776417639569977754844191402927594739069037851707477839207593911886893016618794870530622356073909077832279869798641545167528509966656120623184120128052588408742941658045827255866966100249857968956536613250770326334844204927432961924987891433020671754710428050564671868464658436926086493709176888821257183419013229795869757265111599482263223604228286513011751601176504567030118257385997460972803240338899836840030438830725520798480181575861397469056536579877274090338750406459700907704031830137890544492015701251066934352867527112361743047684237105216779177819594030160887368311805926405114938744235859610328064947158936962470654636736991567663705830950312548447653861922078087824048793236971354828540758657075837209006713701763902429652486225300535997260665898927924843608750347193892239342462507130025307878412116604096773706728162016134101751551184021079984480254041743057914746472840768175369369852937574401874295943063507273467384747124843744395375119899278823903202010381949145094804675442110869084589592876721655764753871572233276245590041302887094585204427900634246823674277680009401177473636685542700515621164233992970974893989913447733956146698563285998205950467321954304'; diff --git a/common/src/utils/bytes.ts b/common/src/utils/bytes.ts index aa9456447..ffd9deeca 100644 --- a/common/src/utils/bytes.ts +++ b/common/src/utils/bytes.ts @@ -1,12 +1,12 @@ import { MAX_BYTES_IN_FIELD } from '../constants/constants.js'; export function bigIntToChunkedBytes( - num: BigInt | bigint, + num: bigint | bigint, bytesPerChunk: number, numChunks: number ) { const res: string[] = []; - const bigintNum: bigint = typeof num == 'bigint' ? num : num.valueOf(); + const bigintNum: bigint = typeof num == 'bigint' ? num : BigInt(num); const msk = (1n << BigInt(bytesPerChunk)) - 1n; for (let i = 0; i < numChunks; ++i) { res.push(((bigintNum >> BigInt(i * bytesPerChunk)) & msk).toString()); diff --git a/common/src/utils/hash.test.ts b/common/src/utils/hash.test.ts new file mode 100644 index 000000000..09c0afe39 --- /dev/null +++ b/common/src/utils/hash.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; + +import { calculateUserIdentifierHash } from './hash'; + +describe('calculateUserIdentifierHash', () => { + it('should return a bigint', () => { + const result = calculateUserIdentifierHash( + 1, + '550e8400-e29b-41d4-a716-446655440000', + 'some data' + ); + expect(typeof result).toBe('bigint'); + }); + + it('should return the same hash for identical inputs', () => { + const destChainID = 42; + const userID = 'abcdef12-3456-7890-abcd-ef1234567890'; + const userDefinedData = 'Test data'; + const hash1 = calculateUserIdentifierHash(destChainID, userID, userDefinedData); + const hash2 = calculateUserIdentifierHash(destChainID, userID, userDefinedData); + expect(hash1).toBe(hash2); + expect(hash1).toMatchInlineSnapshot(`525133570835708563534412370019423387022853755228n`); + }); + + it('should return different hash for different inputs', () => { + const hash1 = calculateUserIdentifierHash( + 42, + 'abcdef12-3456-7890-abcd-ef1234567890', + 'Test data' + ); + const hash2 = calculateUserIdentifierHash( + 42, + 'abcdef12-3456-7890-abcd-ef1234567890', + 'Different data' + ); + expect(hash1).not.toBe(hash2); + expect(hash1).toMatchInlineSnapshot(`525133570835708563534412370019423387022853755228n`); + }); + it('should handle user ids starting with 0x', () => { + const hash1 = calculateUserIdentifierHash(42, '0xabcdef1234567890', 'Test data'); + const hash2 = calculateUserIdentifierHash(42, 'abcdef1234567890', 'Test data'); + expect(hash1).toBe(hash2); + expect(hash1).toMatchInlineSnapshot(`830654111289877969679298811043657652615780822337n`); + }); +}); diff --git a/common/src/utils/hash.ts b/common/src/utils/hash.ts index 07f2cd8e1..0f0876a3f 100644 --- a/common/src/utils/hash.ts +++ b/common/src/utils/hash.ts @@ -29,7 +29,7 @@ export function calculateUserIdentifierHash( destChainID: number, userID: string, userDefinedData: string -): BigInt { +): bigint { const solidityPackedUserContextData = getSolidityPackedUserContextData( destChainID, userID, @@ -133,7 +133,8 @@ export function getSolidityPackedUserContextData( ['bytes32', 'bytes32', 'bytes'], [ ethers.zeroPadValue(ethers.toBeHex(destChainID), 32), - ethers.zeroPadValue('0x' + userIdHex, 32), + + ethers.zeroPadValue(userIdHex.startsWith('0x') ? userIdHex : '0x' + userIdHex, 32), ethers.toUtf8Bytes(userDefinedData), ] ); diff --git a/common/src/utils/trees.ts b/common/src/utils/trees.ts index e1e72fa4a..25b5e0423 100644 --- a/common/src/utils/trees.ts +++ b/common/src/utils/trees.ts @@ -2,14 +2,15 @@ import countries from 'i18n-iso-countries'; // @ts-ignore import en from 'i18n-iso-countries/langs/en.json' with { type: 'json' }; import { - poseidon12, - poseidon13, poseidon2, poseidon3, poseidon5, poseidon6, poseidon10, + poseidon12, + poseidon13, } from 'poseidon-lite'; + import { CSCA_TREE_DEPTH, DSC_TREE_DEPTH, @@ -17,6 +18,7 @@ import { max_dsc_bytes, OFAC_TREE_LEVELS, } from '../constants/constants.js'; +import { packBytes } from './bytes.js'; import type { CertificateData } from './certificate_parsing/dataStructure.js'; import { parseCertificateSimple } from './certificate_parsing/parseCertificateSimple.js'; import { stringToAsciiBigIntArray } from './circuits/uuid.js'; @@ -26,7 +28,6 @@ import { DscCertificateMetaData, parseDscCertificateData, } from './passports/passport_parsing/parseDscCertificateData.js'; -import { packBytes } from './bytes.js'; import { IMT } from '@openpassport/zk-kit-imt'; import { LeanIMT } from '@openpassport/zk-kit-lean-imt'; @@ -36,6 +37,52 @@ import { SMT } from '@openpassport/zk-kit-smt'; // SideEffect here countries.registerLocale(en); +//--------------------------- +// AADHAAR +//--------------------------- +export function buildAadhaarSMT(field: any[], treetype: string): [number, number, SMT] { + let count = 0; + let startTime = performance.now(); + + const hash2 = (childNodes: ChildNodes) => + childNodes.length === 2 ? poseidon2(childNodes) : poseidon3(childNodes); + const tree = new SMT(hash2, true); + + for (let i = 0; i < field.length; i++) { + const entry = field[i]; + + if (i !== 0) { + console.log('Processing', treetype, 'number', i, 'out of', field.length); + } + + let leaf = BigInt(0); + let reverse_leaf = BigInt(0); + if (treetype == 'name_and_dob') { + leaf = processNameAndDobAadhaar(entry, i); + reverse_leaf = processNameAndDobAadhaar(entry, i, true); + } else if (treetype == 'name_and_yob') { + leaf = processNameAndYobAadhaar(entry, i); + reverse_leaf = processNameAndYobAadhaar(entry, i, true); + } + + if (leaf == BigInt(0) || tree.createProof(leaf).membership) { + console.log('This entry already exists in the tree, skipping...'); + continue; + } + + count += 1; + tree.add(leaf, BigInt(1)); + if (reverse_leaf == BigInt(0) || tree.createProof(reverse_leaf).membership) { + console.log('This entry already exists in the tree, skipping...'); + continue; + } + tree.add(reverse_leaf, BigInt(1)); + count += 1; + } + + return [count, performance.now() - startTime, tree]; +} + // SMT trees for 3 levels of matching : // 1. Passport Number and Nationality tree : level 3 (Absolute Match) // 2. Name and date of birth combo tree : level 2 (High Probability Match) @@ -266,12 +313,6 @@ export function getLeafCscaTree(csca_parsed: CertificateData): string { return getLeaf(csca_parsed, 'csca'); } -export function getLeafDscTree(dsc_parsed: CertificateData, csca_parsed: CertificateData): string { - const dscLeaf = getLeaf(dsc_parsed, 'dsc'); - const cscaLeaf = getLeaf(csca_parsed, 'csca'); - return poseidon2([dscLeaf, cscaLeaf]).toString(); -} - function processPassportNoAndNationality( passno: string, nationality: string, @@ -480,6 +521,12 @@ function processCountry(country1: string, country2: string, i: number) { return leaf; } +export function getLeafDscTree(dsc_parsed: CertificateData, csca_parsed: CertificateData): string { + const dscLeaf = getLeaf(dsc_parsed, 'dsc'); + const cscaLeaf = getLeaf(csca_parsed, 'csca'); + return poseidon2([dscLeaf, cscaLeaf]).toString(); +} + export function getLeafDscTreeFromDscCertificateMetadata( dscParsed: CertificateData, dscMetaData: DscCertificateMetaData @@ -501,6 +548,18 @@ export function getNameDobLeaf( return generateSmallKey(poseidon2([getDobLeaf(dobMrz), getNameLeaf(nameMrz)])); } +export const getNameDobLeafAadhaar = (name: string, year: string, month: string, day: string) => { + const paddedName = name + .toUpperCase() + .padEnd(62, '\0') + .split('') + .map((char) => char.charCodeAt(0)); + const namePacked = packBytes(paddedName); + return generateSmallKey( + poseidon5([namePacked[0], namePacked[1], BigInt(year), BigInt(month), BigInt(day)]) + ); +}; + export function getNameLeaf(nameMrz: (bigint | number)[], i?: number): bigint { const middleChunks: bigint[] = []; const chunks: (number | bigint)[][] = []; @@ -544,76 +603,6 @@ export function getNameYobLeaf( return generateSmallKey(poseidon2([getYearLeaf(yobMrz), getNameLeaf(nameMrz)])); } -export function getPassportNumberAndNationalityLeaf( - passport: (bigint | number)[], - nationality: (bigint | number)[], - i?: number -): bigint { - if (passport.length !== 9) { - console.log('parsed passport length is not 9:', i, passport); - return; - } - if (nationality.length !== 3) { - console.log('parsed nationality length is not 3:', i, nationality); - return; - } - try { - const fullHash = poseidon12(passport.concat(nationality)); - return generateSmallKey(fullHash); - } catch (err) { - console.log('err : passport', err, i, passport); - } -} - -//--------------------------- - -// AADHAAR - -//--------------------------- - -export function buildAadhaarSMT(field: any[], treetype: string): [number, number, SMT] { - let count = 0; - let startTime = performance.now(); - - const hash2 = (childNodes: ChildNodes) => - childNodes.length === 2 ? poseidon2(childNodes) : poseidon3(childNodes); - const tree = new SMT(hash2, true); - - for (let i = 0; i < field.length; i++) { - const entry = field[i]; - - if (i !== 0) { - console.log('Processing', treetype, 'number', i, 'out of', field.length); - } - - let leaf = BigInt(0); - let reverse_leaf = BigInt(0); - if (treetype == 'name_and_dob') { - leaf = processNameAndDobAadhaar(entry, i); - reverse_leaf = processNameAndDobAadhaar(entry, i, true); - } else if (treetype == 'name_and_yob') { - leaf = processNameAndYobAadhaar(entry, i); - reverse_leaf = processNameAndYobAadhaar(entry, i, true); - } - - if (leaf == BigInt(0) || tree.createProof(leaf).membership) { - console.log('This entry already exists in the tree, skipping...'); - continue; - } - - count += 1; - tree.add(leaf, BigInt(1)); - if (reverse_leaf == BigInt(0) || tree.createProof(reverse_leaf).membership) { - console.log('This entry already exists in the tree, skipping...'); - continue; - } - tree.add(reverse_leaf, BigInt(1)); - count += 1; - } - - return [count, performance.now() - startTime, tree]; -} - const processNameAndDobAadhaar = (entry: any, i: number, reverse: boolean = false): bigint => { let firstName = entry.First_Name; let lastName = entry.Last_Name; @@ -684,18 +673,6 @@ const processDobAadhaar = (year: string, month: string, day: string): bigint[] = return [year, month, day].map(BigInt); }; -export const getNameDobLeafAadhaar = (name: string, year: string, month: string, day: string) => { - const paddedName = name - .toUpperCase() - .padEnd(62, '\0') - .split('') - .map((char) => char.charCodeAt(0)); - const namePacked = packBytes(paddedName); - return generateSmallKey( - poseidon5([namePacked[0], namePacked[1], BigInt(year), BigInt(month), BigInt(day)]) - ); -}; - export const getNameYobLeafAahaar = (name: string, year: string) => { const paddedName = name .toUpperCase() @@ -706,3 +683,24 @@ export const getNameYobLeafAahaar = (name: string, year: string) => { return generateSmallKey(poseidon3([namePacked[0], namePacked[1], BigInt(year)])); }; + +export function getPassportNumberAndNationalityLeaf( + passport: (bigint | number)[], + nationality: (bigint | number)[], + i?: number +): bigint { + if (passport.length !== 9) { + console.log('parsed passport length is not 9:', i, passport); + return; + } + if (nationality.length !== 3) { + console.log('parsed nationality length is not 3:', i, nationality); + return; + } + try { + const fullHash = poseidon12(passport.concat(nationality)); + return generateSmallKey(fullHash); + } catch (err) { + console.log('err : passport', err, i, passport); + } +} diff --git a/package.json b/package.json index 7203c7a10..9503cffb0 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "build": "yarn workspaces foreach --topological-dev --parallel --exclude @selfxyz/contracts -i --all run build", "check:versions": "node scripts/check-package-versions.mjs", "format": "SKIP_BUILD_DEPS=1 yarn format:root && yarn format:github && SKIP_BUILD_DEPS=1 yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run format", - "format:github": "prettier --parser yaml --write .github/**/*.yml --single-quote false", - "format:root": "prettier --parser markdown --write *.md scripts/**/*.md && prettier --parser yaml --write .*.{yml,yaml} --single-quote false && prettier --write scripts/**/*.{js,mjs,ts} && prettier --parser json --write scripts/**/*.json", + "format:github": "yarn prettier --parser yaml --write .github/**/*.yml --single-quote false", + "format:root": "echo 'format markdown' && yarn prettier --parser markdown --write *.md && echo 'format yaml' && yarn prettier --parser yaml --write .*.{yml,yaml} --single-quote false && yarn prettier --write scripts/**/*.{js,mjs,ts} && yarn prettier --parser json --write scripts/**/*.json", "gitleaks": "gitleaks protect --staged --redact --config=.gitleaks.toml", "postinstall": "node scripts/run-patch-package.cjs", "lint": "yarn lint:headers && yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run lint", @@ -46,6 +46,7 @@ "husky": "9.1.7", "knip": "^5.62.0", "patch-package": "^8.0.0", + "prettier": "^3.5.3", "typescript": "^5.9.2" }, "packageManager": "yarn@4.6.0", diff --git a/packages/mobile-sdk-alpha/package.json b/packages/mobile-sdk-alpha/package.json index 885973bb9..e7b698ee7 100644 --- a/packages/mobile-sdk-alpha/package.json +++ b/packages/mobile-sdk-alpha/package.json @@ -52,8 +52,8 @@ "demo:ios": "yarn workspace demo-app ios", "demo:start": "yarn workspace demo-app start", "demo:test": "yarn workspace demo-app test", - "fmt": "prettier --check .", - "fmt:fix": "prettier --write .", + "fmt": "yarn prettier --check .", + "fmt:fix": "yarn prettier --write .", "format": "sh -c 'if [ -z \"$SKIP_BUILD_DEPS\" ]; then yarn nice; else yarn fmt:fix; fi'", "lint": "eslint .", "lint:fix": "eslint --fix .", @@ -70,6 +70,7 @@ }, "dependencies": { "@selfxyz/common": "workspace:^", + "socket.io-client": "^4.8.1", "tslib": "^2.6.2", "zustand": "^4.5.2" }, diff --git a/packages/mobile-sdk-alpha/src/browser.ts b/packages/mobile-sdk-alpha/src/browser.ts index 5810a4bcc..212b89c6a 100644 --- a/packages/mobile-sdk-alpha/src/browser.ts +++ b/packages/mobile-sdk-alpha/src/browser.ts @@ -63,6 +63,8 @@ export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz'; export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator'; +export { generateTEEInputsDisclose } from './processing/generate-disclosure-inputs'; + // Core functions export { isPassportDataValid } from './validation/document'; @@ -73,5 +75,4 @@ export { parseNFCResponse, scanNFC } from './nfc'; export { reactNativeScannerAdapter } from './adapters/react-native/scanner'; export { scanQRProof } from './qr'; - export { webScannerShim } from './adapters/web/shims'; diff --git a/packages/mobile-sdk-alpha/src/index.ts b/packages/mobile-sdk-alpha/src/index.ts index afc18791a..a500aff2c 100644 --- a/packages/mobile-sdk-alpha/src/index.ts +++ b/packages/mobile-sdk-alpha/src/index.ts @@ -98,6 +98,10 @@ export { formatDateToYYMMDD, scanMRZ } from './mrz'; export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator'; +export { generateTEEInputsDisclose } from './processing/generate-disclosure-inputs'; + +// Documents utils + // Core functions export { isPassportDataValid } from './validation/document'; diff --git a/packages/mobile-sdk-alpha/src/processing/generate-disclosure-inputs.ts b/packages/mobile-sdk-alpha/src/processing/generate-disclosure-inputs.ts new file mode 100644 index 000000000..24c421a9a --- /dev/null +++ b/packages/mobile-sdk-alpha/src/processing/generate-disclosure-inputs.ts @@ -0,0 +1,30 @@ +// 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 { DocumentCategory, PassportData } from '@selfxyz/common/types'; +import type { SelfApp } from '@selfxyz/common/utils'; +import { generateTEEInputsDiscloseStateless } from '@selfxyz/common/utils/circuits/registerInputs'; + +import { useProtocolStore } from '../stores/protocolStore'; + +export function generateTEEInputsDisclose(secret: string, passportData: PassportData, selfApp: SelfApp) { + return generateTEEInputsDiscloseStateless(secret, passportData, selfApp, (document: DocumentCategory, tree) => { + const protocolStore = useProtocolStore.getState(); + const docStore = (protocolStore as any)[document]; + if (!docStore) { + throw new Error(`Unknown or unloaded document category in protocol store: ${document}`); + } + switch (tree) { + case 'ofac': + return docStore.ofac_trees; + case 'commitment': + if (!docStore.commitment_tree) { + throw new Error('Commitment tree not loaded'); + } + return docStore.commitment_tree; + default: + throw new Error('Unknown tree type'); + } + }); +} diff --git a/packages/mobile-sdk-alpha/src/stores/index.ts b/packages/mobile-sdk-alpha/src/stores/index.ts index bf1168dd4..91670f18c 100644 --- a/packages/mobile-sdk-alpha/src/stores/index.ts +++ b/packages/mobile-sdk-alpha/src/stores/index.ts @@ -3,3 +3,4 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. export { useProtocolStore } from './protocolStore'; +export { useSelfAppStore } from './selfAppStore'; diff --git a/app/src/stores/selfAppStore.tsx b/packages/mobile-sdk-alpha/src/stores/selfAppStore.tsx similarity index 88% rename from app/src/stores/selfAppStore.tsx rename to packages/mobile-sdk-alpha/src/stores/selfAppStore.tsx index c87ade3ef..379ee87ad 100644 --- a/app/src/stores/selfAppStore.tsx +++ b/packages/mobile-sdk-alpha/src/stores/selfAppStore.tsx @@ -17,11 +17,7 @@ interface SelfAppState { cleanSelfApp: () => void; setSelfApp: (selfApp: SelfApp | null) => void; _initSocket: (sessionId: string) => Socket; - handleProofResult: ( - proof_verified: boolean, - error_code?: string, - reason?: string, - ) => void; + handleProofResult: (proof_verified: boolean, error_code?: string, reason?: string) => void; } export const useSelfAppStore = create((set, get) => ({ @@ -30,9 +26,7 @@ export const useSelfAppStore = create((set, get) => ({ socket: null, _initSocket: (sessionId: string): Socket => { - const connectionUrl = WS_DB_RELAYER.startsWith('https') - ? WS_DB_RELAYER.replace(/^https/, 'wss') - : WS_DB_RELAYER; + const connectionUrl = WS_DB_RELAYER.startsWith('https') ? WS_DB_RELAYER.replace(/^https/, 'wss') : WS_DB_RELAYER; const socketUrl = `${connectionUrl}/websocket`; // Create a new socket connection using the updated URL. @@ -72,8 +66,7 @@ export const useSelfAppStore = create((set, get) => ({ // Listen for the event only once per connection attempt socket.once('self_app', (data: unknown) => { try { - const appData: SelfApp = - typeof data === 'string' ? JSON.parse(data) : (data as SelfApp); + const appData: SelfApp = typeof data === 'string' ? JSON.parse(data) : (data as SelfApp); // Basic validation if (!appData || typeof appData !== 'object' || !appData.sessionId) { @@ -130,18 +123,12 @@ export const useSelfAppStore = create((set, get) => ({ set({ selfApp: null, sessionId: null, socket: null }); }, - handleProofResult: ( - proof_verified: boolean, - error_code?: string, - reason?: string, - ) => { + handleProofResult: (proof_verified: boolean, error_code?: string, reason?: string) => { const socket = get().socket; const sessionId = get().sessionId; if (!socket || !sessionId) { - console.error( - '[SelfAppStore] Cannot handleProofResult: Socket or SessionId missing.', - ); + console.error('[SelfAppStore] Cannot handleProofResult: Socket or SessionId missing.'); return; } diff --git a/packages/mobile-sdk-alpha/tests/processing/generate-disclosure-inputs.test.ts b/packages/mobile-sdk-alpha/tests/processing/generate-disclosure-inputs.test.ts new file mode 100644 index 000000000..c2c7c24b0 --- /dev/null +++ b/packages/mobile-sdk-alpha/tests/processing/generate-disclosure-inputs.test.ts @@ -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. + +/** + * @vitest-environment node + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { PassportData, SelfApp } from '@selfxyz/common'; + +import { generateTEEInputsDisclose } from '../../src/processing/generate-disclosure-inputs'; +import { useProtocolStore } from '../../src/stores/protocolStore'; +// Mocks for dependencies +const mockSecret = '0x' + '00'.repeat(30) + 'a4ec'; // 32-byte hex string +const mockPassportData: PassportData = { + mrz: 'P ({ + useProtocolStore: { + getState: () => ({ + passport: { + ofac_trees: { + nameAndDob: '{"root":["0"]}', + nameAndYob: '{"root":["0"]}', + passportNoAndNationality: '{"root":["0"]}', + }, + commitment_tree: '[[]]', + }, + }), + }, +})); + +describe('generateTEEInputsDisclose', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it('throws error for unknown document category', () => { + // Mock the store to return an unknown document category + vi.spyOn(useProtocolStore, 'getState').mockReturnValue({ + unknown: undefined, + } as any); + + expect(() => generateTEEInputsDisclose(mockSecret, mockPassportData, mockSelfApp)).toThrowError( + `Unknown or unloaded document category in protocol store: passport`, + ); + }); + + it('throws error for unknown tree type', () => { + // This test doesn't make sense as written since tree type is determined internally + // Let's test the commitment tree validation instead + vi.spyOn(useProtocolStore, 'getState').mockReturnValue({ + passport: { + ofac_trees: 'ofac-tree-data', + commitment_tree: undefined, + }, + } as any); + + expect(() => generateTEEInputsDisclose(mockSecret, mockPassportData, mockSelfApp)).toThrowError( + `Invalid OFAC tree structure: missing required fields`, + ); + }); + + it('throws error if commitment tree not loaded', () => { + vi.spyOn(useProtocolStore, 'getState').mockReturnValue({ + passport: { + ofac_trees: 'ofac-tree-data', + commitment_tree: undefined, + }, + } as any); + + expect(() => generateTEEInputsDisclose(mockSecret, mockPassportData, mockSelfApp)).toThrowError( + `Invalid OFAC tree structure: missing required fields`, + ); + }); +}); diff --git a/yarn.lock b/yarn.lock index 46760b3bb..d10e517f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5038,6 +5038,7 @@ __metadata: eslint-plugin-sort-exports: "npm:^0.8.0" jsdom: "npm:^24.0.0" prettier: "npm:^3.5.3" + socket.io-client: "npm:^4.8.1" tslib: "npm:^2.6.2" tsup: "npm:^8.0.1" typescript: "npm:^5.9.2" @@ -23234,6 +23235,7 @@ __metadata: husky: "npm:9.1.7" knip: "npm:^5.62.0" patch-package: "npm:^8.0.0" + prettier: "npm:^3.5.3" react: "npm:^18.3.1" react-native: "npm:0.76.9" typescript: "npm:^5.9.2" From b335e668f40f888254050e4ce920faf1bd03468e Mon Sep 17 00:00:00 2001 From: Aaron DeRuvo Date: Thu, 11 Sep 2025 19:36:45 +0200 Subject: [PATCH 06/25] chore(mobile-sdk-alpha): remove unused tslib dependency (#1053) * remove tslib -- seems unused * remove deps accidentally added to root * build file --- .gitignore | 1 + package.json | 4 ---- packages/mobile-sdk-alpha/package.json | 1 - yarn.lock | 5 +---- 4 files changed, 2 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 5cfc10170..3bea386a9 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ output/* mobile-sdk-alpha-ci.tgz **/mobile-sdk-alpha-*.tgz /tmp/mobile-sdk-alpha*.tgz +dataInput.d.ts diff --git a/package.json b/package.json index 9503cffb0..8e4532693 100644 --- a/package.json +++ b/package.json @@ -36,10 +36,6 @@ "react": "^18.3.1", "react-native": "0.76.9" }, - "dependencies": { - "react": "^18.3.1", - "react-native": "0.76.9" - }, "devDependencies": { "@types/node": "^22.0.0", "gitleaks": "1.0.0", diff --git a/packages/mobile-sdk-alpha/package.json b/packages/mobile-sdk-alpha/package.json index e7b698ee7..ab73cc0ef 100644 --- a/packages/mobile-sdk-alpha/package.json +++ b/packages/mobile-sdk-alpha/package.json @@ -71,7 +71,6 @@ "dependencies": { "@selfxyz/common": "workspace:^", "socket.io-client": "^4.8.1", - "tslib": "^2.6.2", "zustand": "^4.5.2" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index d10e517f5..2df2e2ba8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5039,7 +5039,6 @@ __metadata: jsdom: "npm:^24.0.0" prettier: "npm:^3.5.3" socket.io-client: "npm:^4.8.1" - tslib: "npm:^2.6.2" tsup: "npm:^8.0.1" typescript: "npm:^5.9.2" vitest: "npm:^1.6.0" @@ -23236,8 +23235,6 @@ __metadata: knip: "npm:^5.62.0" patch-package: "npm:^8.0.0" prettier: "npm:^3.5.3" - react: "npm:^18.3.1" - react-native: "npm:0.76.9" typescript: "npm:^5.9.2" languageName: unknown linkType: soft @@ -25210,7 +25207,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:^2.6.3, tslib@npm:^2.8.0, tslib@npm:^2.8.1": +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.6.3, tslib@npm:^2.8.0, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 From e1a58256df034bbed30976b7b8ce42781384efa7 Mon Sep 17 00:00:00 2001 From: Aaron DeRuvo Date: Thu, 11 Sep 2025 22:01:45 +0200 Subject: [PATCH 07/25] remove unused imports (#1055) --- app/src/components/NavBar/HomeNavBar.tsx | 2 +- app/src/components/NavBar/IdDetailsNavBar.tsx | 3 +-- .../homeScreen/SvgXmlWrapper.web.tsx | 11 +++++++--- app/src/components/homeScreen/idCard.tsx | 21 ++++++------------- 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/app/src/components/NavBar/HomeNavBar.tsx b/app/src/components/NavBar/HomeNavBar.tsx index 0adc5030f..6513b19e8 100644 --- a/app/src/components/NavBar/HomeNavBar.tsx +++ b/app/src/components/NavBar/HomeNavBar.tsx @@ -16,7 +16,7 @@ import { NavBar } from '@/components/NavBar/BaseNavBar'; import ActivityIcon from '@/images/icons/activity.svg'; import ScanIcon from '@/images/icons/qr_scan.svg'; import SettingsIcon from '@/images/icons/settings.svg'; -import { black, charcoal, neutral400, slate50, white } from '@/utils/colors'; +import { black, charcoal, slate50 } from '@/utils/colors'; import { extraYPadding } from '@/utils/constants'; import { buttonTap } from '@/utils/haptic'; diff --git a/app/src/components/NavBar/IdDetailsNavBar.tsx b/app/src/components/NavBar/IdDetailsNavBar.tsx index 7ae0cbf0e..8bc02e082 100644 --- a/app/src/components/NavBar/IdDetailsNavBar.tsx +++ b/app/src/components/NavBar/IdDetailsNavBar.tsx @@ -4,9 +4,8 @@ import React from 'react'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { Button, Text, View, XStack } from 'tamagui'; +import { Button, Text, View } from 'tamagui'; import type { NativeStackHeaderProps } from '@react-navigation/native-stack'; -import { ChevronLeft } from '@tamagui/lucide-icons'; import { NavBar } from '@/components/NavBar/BaseNavBar'; import { black, charcoal, slate50 } from '@/utils/colors'; diff --git a/app/src/components/homeScreen/SvgXmlWrapper.web.tsx b/app/src/components/homeScreen/SvgXmlWrapper.web.tsx index bf91dbbea..0ff28c25e 100644 --- a/app/src/components/homeScreen/SvgXmlWrapper.web.tsx +++ b/app/src/components/homeScreen/SvgXmlWrapper.web.tsx @@ -3,14 +3,19 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. import DOMPurify from 'dompurify'; -import React, { createElement, forwardRef } from 'react'; +import { + createElement, + type CSSProperties, + forwardRef, + type HTMLAttributes, +} from 'react'; type Props = { xml: string; width?: number; height?: number; - style?: React.CSSProperties; -} & React.HTMLAttributes; + style?: CSSProperties; +} & HTMLAttributes; export const SvgXml = forwardRef( ({ xml, width, height, style, ...props }, ref) => { diff --git a/app/src/components/homeScreen/idCard.tsx b/app/src/components/homeScreen/idCard.tsx index 13eb77d9a..64fa39f36 100644 --- a/app/src/components/homeScreen/idCard.tsx +++ b/app/src/components/homeScreen/idCard.tsx @@ -2,30 +2,21 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import React, { useCallback, useState } from 'react'; -import { Dimensions, Pressable } from 'react-native'; -import { Button, Image, Separator, Text, XStack, YStack } from 'tamagui'; -import { useFocusEffect } from '@react-navigation/native'; +import type { FC } from 'react'; +import { Dimensions } from 'react-native'; +import { Separator, Text, XStack, YStack } from 'tamagui'; import { attributeToPosition, attributeToPosition_ID, } from '@selfxyz/common/constants'; import { PassportData } from '@selfxyz/common/types'; -import { formatMrz } from '@selfxyz/common/utils'; -import { pad } from '@selfxyz/common/utils/passports/passport'; import { SvgXml } from '@/components/homeScreen/SvgXmlWrapper'; import EPassport from '@/images/icons/epassport.svg'; import LogoGray from '@/images/logo_gray.svg'; -import LogoInversed from '@/images/logo_inversed.svg'; -import { usePassport } from '@/providers/passportDataProvider'; import { black, - blue600, - green500, - red500, - slate50, slate100, slate300, slate400, @@ -53,7 +44,7 @@ interface IdCardLayoutAttributes { // each element size should be determined as % of the screen or the parent element // the border radius should be adaptative too, as well as the padding // this is the v0 of this component so we should only use placholders for now, no need to pass the real passport data as parameters. -const IdCardLayout: React.FC = ({ +const IdCardLayout: FC = ({ idDocument, selected, hidden, @@ -64,7 +55,7 @@ const IdCardLayout: React.FC = ({ }; // Get screen dimensions for adaptive sizing - const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); + const { width: screenWidth } = Dimensions.get('window'); // Calculate adaptive sizes based on screen dimensions // Reduce width slightly to account for horizontal margins (8px each side = 16px total) @@ -442,7 +433,7 @@ interface IdAttributeProps { // the font size should adapt according to the size available to fit perfectly. // only svg are allowed. // each element size should be determined as % of the screen or the parent element -const IdAttribute: React.FC = ({ +const IdAttribute: FC = ({ name, value, maskValue, From 846956d28ef4c481d084830fa816a225fcf016e4 Mon Sep 17 00:00:00 2001 From: Nesopie <87437291+Nesopie@users.noreply.github.com> Date: Fri, 12 Sep 2025 08:32:45 +0530 Subject: [PATCH 08/25] fix: sha256 signed attr tests (#1058) --- circuits/tests/register/register.test.ts | 4 +- .../tests/register_id/register_id.test.ts | 4 +- common/index.ts | 1 + common/src/constants/constants.ts | 8 ++ common/src/utils/circuits/generateInputs.ts | 73 +++++++++++++++++++ common/src/utils/index.ts | 1 + 6 files changed, 87 insertions(+), 4 deletions(-) diff --git a/circuits/tests/register/register.test.ts b/circuits/tests/register/register.test.ts index dfd2dbda7..f4572667e 100644 --- a/circuits/tests/register/register.test.ts +++ b/circuits/tests/register/register.test.ts @@ -8,7 +8,7 @@ import serialized_dsc_tree from '@selfxyz/common/pubkeys/serialized_dsc_tree.jso import { PASSPORT_ATTESTATION_ID } from '@selfxyz/common/constants/constants'; import { parseCertificateSimple } from '@selfxyz/common/utils/certificate_parsing/parseCertificateSimple'; import { getCircuitNameFromPassportData } from '@selfxyz/common/utils/circuits/circuitsName'; -import { generateCircuitInputsRegister } from '@selfxyz/common/utils/circuits/generateInputs'; +import { generateCircuitInputsRegisterForTests } from '@selfxyz/common/utils/circuits/generateInputs'; import { genAndInitMockPassportData } from '@selfxyz/common/utils/passports/genMockPassportData'; import { generateCommitment, generateNullifier } from '@selfxyz/common/utils/passports/passport'; import { SignatureAlgorithm } from '@selfxyz/common/utils/types'; @@ -48,7 +48,7 @@ testSuite.forEach( const secret = poseidon6('SECRET'.split('').map((x) => BigInt(x.charCodeAt(0)))).toString(); - const inputs = generateCircuitInputsRegister( + const inputs = generateCircuitInputsRegisterForTests( secret, passportData, serialized_dsc_tree as string diff --git a/circuits/tests/register_id/register_id.test.ts b/circuits/tests/register_id/register_id.test.ts index 2d2c57554..dfab1d468 100644 --- a/circuits/tests/register_id/register_id.test.ts +++ b/circuits/tests/register_id/register_id.test.ts @@ -3,7 +3,7 @@ import { describe } from 'mocha'; import { expect } from 'chai'; import path from 'path'; import { wasm as wasm_tester } from 'circom_tester'; -import { generateCircuitInputsRegister } from '@selfxyz/common/utils/circuits/generateInputs'; +import { generateCircuitInputsRegisterForTests } from '@selfxyz/common/utils/circuits/generateInputs'; import { SignatureAlgorithm } from '@selfxyz/common/utils/types'; import { getCircuitNameFromPassportData } from '@selfxyz/common/utils/circuits/circuitsName'; import { sigAlgs, fullSigAlgs } from './test_cases.js'; @@ -45,7 +45,7 @@ testSuite.forEach( }); const secret = poseidon6('SECRET'.split('').map((x) => BigInt(x.charCodeAt(0)))).toString(); - const inputs = generateCircuitInputsRegister( + const inputs = generateCircuitInputsRegisterForTests( secret, passportData, serialized_dsc_tree as string diff --git a/common/index.ts b/common/index.ts index add454242..15ef480ca 100644 --- a/common/index.ts +++ b/common/index.ts @@ -65,6 +65,7 @@ export { genMockIdDocAndInitDataParsing, generateCircuitInputsDSC, generateCircuitInputsRegister, + generateCircuitInputsRegisterForTests, generateCircuitInputsVCandDisclose, generateCommitment, generateMockDSC, diff --git a/common/src/constants/constants.ts b/common/src/constants/constants.ts index ae6a93d93..d80134cd8 100644 --- a/common/src/constants/constants.ts +++ b/common/src/constants/constants.ts @@ -131,6 +131,14 @@ export const MAX_PADDED_ECONTENT_LEN: Partial = { + sha1: 128, + sha224: 128, + sha256: 256, + sha384: 256, + sha512: 256, +}; + export const MAX_PADDED_SIGNED_ATTR_LEN: Record<(typeof hashAlgos)[number], number> = { sha1: 128, sha224: 128, diff --git a/common/src/utils/circuits/generateInputs.ts b/common/src/utils/circuits/generateInputs.ts index 0c863db03..35a0d3ab6 100644 --- a/common/src/utils/circuits/generateInputs.ts +++ b/common/src/utils/circuits/generateInputs.ts @@ -4,6 +4,7 @@ import { max_dsc_bytes, MAX_PADDED_ECONTENT_LEN, MAX_PADDED_SIGNED_ATTR_LEN, + MAX_PADDED_SIGNED_ATTR_LEN_FOR_TESTS, OFAC_TREE_LEVELS, } from '../../constants/constants.js'; import { getCurrentDateYYMMDD } from '../date.js'; @@ -200,6 +201,78 @@ export function generateCircuitInputsOfac( }; } +export function generateCircuitInputsRegisterForTests( + secret: string, + passportData: PassportData, + serializedDscTree: string +) { + const { mrz, eContent, signedAttr } = passportData; + const passportMetadata = passportData.passportMetadata; + const dscParsed = passportData.dsc_parsed; + + const [dscTbsBytesPadded] = pad(dscParsed.hashAlgorithm)(dscParsed.tbsBytes, max_dsc_bytes); + + const { pubKey, signature, signatureAlgorithmFullName } = getPassportSignatureInfos(passportData); + const mrz_formatted = formatMrz(mrz); + + if (eContent.length > MAX_PADDED_ECONTENT_LEN[signatureAlgorithmFullName]) { + console.error( + `eContent too long (${eContent.length} bytes). Max length is ${MAX_PADDED_ECONTENT_LEN[signatureAlgorithmFullName]} bytes.` + ); + throw new Error( + `This length of datagroups (${eContent.length} bytes) is currently unsupported. Please contact us so we add support!` + ); + } + + const [eContentPadded, eContentLen] = pad(passportMetadata.eContentHashFunction)( + eContent, + MAX_PADDED_ECONTENT_LEN[passportMetadata.dg1HashFunction] + ); + const [signedAttrPadded, signedAttrPaddedLen] = pad(passportMetadata.signedAttrHashFunction)( + signedAttr, + MAX_PADDED_SIGNED_ATTR_LEN_FOR_TESTS[passportMetadata.eContentHashFunction] + ); + + const dsc_leaf = getLeafDscTree(dscParsed, passportData.csca_parsed); // TODO: WRONG + const [root, path, siblings, leaf_depth] = getDscTreeInclusionProof(dsc_leaf, serializedDscTree); + const csca_tree_leaf = getLeafCscaTree(passportData.csca_parsed); + + // Get start index of DSC pubkey based on algorithm + const [startIndex, keyLength] = findStartPubKeyIndex( + dscParsed, + dscTbsBytesPadded, + dscParsed.signatureAlgorithm + ); + + const inputs = { + raw_dsc: dscTbsBytesPadded.map((x) => x.toString()), + raw_dsc_actual_length: [BigInt(dscParsed.tbsBytes.length).toString()], + dsc_pubKey_offset: startIndex, + dsc_pubKey_actual_size: [BigInt(keyLength).toString()], + dg1: mrz_formatted, + dg1_hash_offset: passportMetadata.dg1HashOffset, + eContent: eContentPadded, + eContent_padded_length: eContentLen, + signed_attr: signedAttrPadded, + signed_attr_padded_length: signedAttrPaddedLen, + signed_attr_econtent_hash_offset: passportMetadata.eContentHashOffset, + pubKey_dsc: pubKey, + signature_passport: signature, + merkle_root: [BigInt(root).toString()], + leaf_depth: leaf_depth, + path: path, + siblings: siblings, + csca_tree_leaf: csca_tree_leaf, + secret: secret, + }; + + return Object.entries(inputs) + .map(([key, value]) => ({ + [key]: formatInput(value), + })) + .reduce((acc, curr) => ({ ...acc, ...curr }), {}); +} + export function generateCircuitInputsRegister( secret: string, passportData: PassportData, diff --git a/common/src/utils/index.ts b/common/src/utils/index.ts index e9fcef915..d408abb1f 100644 --- a/common/src/utils/index.ts +++ b/common/src/utils/index.ts @@ -54,6 +54,7 @@ export { export { generateCircuitInputsDSC, generateCircuitInputsRegister, + generateCircuitInputsRegisterForTests, generateCircuitInputsVCandDisclose, } from './circuits/generateInputs.js'; export { getCircuitNameFromPassportData } from './circuits/circuitsName.js'; From 712e78004a3fe2be4273c23b5adf0d9b24e02f1a Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Fri, 12 Sep 2025 08:11:45 -0700 Subject: [PATCH 09/25] fix mock screen launch (#1059) --- app/src/screens/settings/ManageDocumentsScreen.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/screens/settings/ManageDocumentsScreen.tsx b/app/src/screens/settings/ManageDocumentsScreen.tsx index c6d76776f..badff03b6 100644 --- a/app/src/screens/settings/ManageDocumentsScreen.tsx +++ b/app/src/screens/settings/ManageDocumentsScreen.tsx @@ -283,7 +283,6 @@ const ManageDocumentsScreen: React.FC = () => { }; const handleGenerateMock = () => { - if (!__DEV__) return; impactLight(); trackEvent(DocumentEvents.ADD_NEW_MOCK_SELECTED); navigation.navigate('CreateMock'); From 74605c0b44844c039ab877b2bd55afdc4e1ef789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Seshanth=2ES=F0=9F=90=BA?= <35675963+seshanthS@users.noreply.github.com> Date: Sat, 13 Sep 2025 00:41:56 +0530 Subject: [PATCH 10/25] Hotfix: Belgium ID cards (#1061) * feat: parse belgium TD1 mrz android * feat: Parse Belgium TD1 MRZ IOS --- .../com/passportreader/utils/OcrUtils.kt | 74 +++++++-- app/ios/LiveMRZScannerView.swift | 143 ++++++++++++++++-- .../com/selfxyz/selfSDK/utils/OcrUtils.kt | 73 +++++++-- .../ios/SelfSDK/SelfLiveMRZScannerView.swift | 143 ++++++++++++++++-- 4 files changed, 382 insertions(+), 51 deletions(-) diff --git a/app/android/android-passport-reader/app/src/main/java/example/jllarraz/com/passportreader/utils/OcrUtils.kt b/app/android/android-passport-reader/app/src/main/java/example/jllarraz/com/passportreader/utils/OcrUtils.kt index a7c3dc44a..a41e2061d 100644 --- a/app/android/android-passport-reader/app/src/main/java/example/jllarraz/com/passportreader/utils/OcrUtils.kt +++ b/app/android/android-passport-reader/app/src/main/java/example/jllarraz/com/passportreader/utils/OcrUtils.kt @@ -26,9 +26,15 @@ object OcrUtils { private val REGEX_ID_DOCUMENT_NUMBER = "(ID)(?[A-Z<]{3})(?[A-Z0-9<]{9})(?[0-9]{1})" private val REGEX_ID_DATE_OF_BIRTH = "(?[0-9]{6})(?[0-9]{1})(?[FM<]{1})" + // Belgium TD1 (ID Card) specific pattern + private val REGEX_BELGIUM_ID_DOCUMENT_NUMBER = "IDBEL(?[A-Z0-9]{9})<(?[A-Z0-9]{3})(?\\d)" + private val REGEX_BELGIUM_ID_DATE_OF_BIRTH = "(?[0-9]{6})(?[0-9]{1})(?[FM<]{1})(?[0-9]{6})(?[0-9]{1})" + private val patternDocumentNumber = Pattern.compile(REGEX_ID_DOCUMENT_NUMBER) private val patternDateOfBirth = Pattern.compile(REGEX_ID_DATE_OF_BIRTH) private val patternDocumentCode = Pattern.compile(REGEX_ID_DOCUMENT_CODE) + private val patternBelgiumDocumentNumber = Pattern.compile(REGEX_BELGIUM_ID_DOCUMENT_NUMBER) + private val patternBelgiumDateOfBirth = Pattern.compile(REGEX_BELGIUM_ID_DATE_OF_BIRTH) fun processOcr( @@ -50,7 +56,7 @@ object OcrUtils { fullRead += "$temp-" } fullRead = fullRead.uppercase() - Log.d(TAG, "Read: $fullRead") + // Log.d(TAG, "Read: $fullRead") // We try with TD1 format first (ID Card) val patternTD1Line1 = Pattern.compile(REGEX_TD1_LINE1) @@ -69,40 +75,63 @@ object OcrUtils { val matcherDocumentNumber = patternDocumentNumber.matcher(fullRead) val matcherDateOfBirth = patternDateOfBirth.matcher(fullRead) - val hasDocumentNumber = matcherDocumentNumber.find() val hasDateOfBirth = matcherDateOfBirth.find() + // Belgium specific matchers + val matcherBelgiumDocumentNumber = patternBelgiumDocumentNumber.matcher(fullRead) + val hasBelgiumDocumentNumber = matcherBelgiumDocumentNumber.find() + val documentNumber = if (hasDocumentNumber) matcherDocumentNumber.group("documentNumber") else null val checkDigitDocumentNumber = if (hasDocumentNumber) matcherDocumentNumber.group("checkDigitDocumentNumber")?.toIntOrNull() else null val countryCode = if (hasDocumentNumber) matcherDocumentNumber.group("country") else null val dateOfBirth = if (hasDateOfBirth) matcherDateOfBirth.group("dateOfBirth") else null + + // Belgium specific values + val belgiumCheckDigit = if (hasBelgiumDocumentNumber) matcherBelgiumDocumentNumber.group("checkDigit")?.toIntOrNull() else null + val belgiumDateOfBirth = if (hasBelgiumDocumentNumber) { + val dateOfBirthMatcher = patternBelgiumDateOfBirth.matcher(fullRead) + if (dateOfBirthMatcher.find()) dateOfBirthMatcher.group("dateOfBirth") else null + } else null + + // Final values + val finalDocumentNumber = if (hasBelgiumDocumentNumber) { + val doc9 = matcherBelgiumDocumentNumber.group("doc9") + val doc3 = matcherBelgiumDocumentNumber.group("doc3") + val checkDigit = matcherBelgiumDocumentNumber.group("checkDigit") + cleanBelgiumDocumentNumber(doc9, doc3, checkDigit) + } else documentNumber + val finalDateOfBirth = if (hasBelgiumDocumentNumber) belgiumDateOfBirth else dateOfBirth + val finalCountryCode = if (hasBelgiumDocumentNumber) "BEL" else countryCode + val finalCheckDigit = if (hasBelgiumDocumentNumber) belgiumCheckDigit else checkDigitDocumentNumber + val checkDigitDateOfBirth = if (hasDateOfBirth) matcherDateOfBirth.group("checkDigitDateOfBirth")?.toIntOrNull() else null val gender = if (hasDateOfBirth) matcherDateOfBirth.group("gender") else null - val expirationDate: String? = if (!countryCode.isNullOrEmpty()) { - val expirationDateRegex = "(?[0-9]{6})(?[0-9]{1})" + Pattern.quote(countryCode) + val expirationDate: String? = if (!finalCountryCode.isNullOrEmpty()) { + val expirationDateRegex = "(?[0-9]{6})(?[0-9]{1})" + Pattern.quote(finalCountryCode) + // val expirationDateRegex = "(?[0-9]{6})(?[0-9]{1})UTO" val patternExpirationDate = Pattern.compile(expirationDateRegex) val matcherExpirationDate = patternExpirationDate.matcher(fullRead) if (matcherExpirationDate.find()) matcherExpirationDate.group("expirationDate") else null } else null // Only proceed if all required fields are present and non-empty - if (!countryCode.isNullOrEmpty() && !documentNumber.isNullOrEmpty() && !dateOfBirth.isNullOrEmpty() && !expirationDate.isNullOrEmpty() && checkDigitDocumentNumber != null) { - val cleanDocumentNumber = cleanDocumentNumber(documentNumber, checkDigitDocumentNumber) - Log.d(TAG, "cleanDocumentNumber") + if (!finalCountryCode.isNullOrEmpty() && !finalDocumentNumber.isNullOrEmpty() && !finalDateOfBirth.isNullOrEmpty() && !expirationDate.isNullOrEmpty() && finalCheckDigit != null) { + val cleanDocumentNumber = cleanDocumentNumber(finalDocumentNumber, finalCheckDigit) + // Log.d(TAG, "cleanDocumentNumber") if (cleanDocumentNumber != null) { - val mrzInfo = createDummyMrz("ID", countryCode, cleanDocumentNumber, dateOfBirth, expirationDate) + val mrzInfo = createDummyMrz("ID", finalCountryCode, cleanDocumentNumber, finalDateOfBirth, expirationDate) // Log.d(TAG, "MRZ-TD1: $mrzInfo") callback.onMRZRead(mrzInfo, timeRequired) return } } else { - if (countryCode.isNullOrEmpty()) Log.d(TAG, "Missing or invalid countryCode") - if (documentNumber.isNullOrEmpty()) Log.d(TAG, "Missing or invalid documentNumber") - if (dateOfBirth.isNullOrEmpty()) Log.d(TAG, "Missing or invalid dateOfBirth") + if (finalCountryCode.isNullOrEmpty()) Log.d(TAG, "Missing or invalid finalCountryCode") + if (finalDocumentNumber.isNullOrEmpty()) Log.d(TAG, "Missing or invalid finalDocumentNumber") + if (finalDateOfBirth.isNullOrEmpty()) Log.d(TAG, "Missing or invalid dateOfBirth") if (expirationDate.isNullOrEmpty()) Log.d(TAG, "Missing or invalid expirationDate") - if (checkDigitDocumentNumber == null) Log.d(TAG, "Missing or invalid checkDigitDocumentNumber") + if (finalCheckDigit == null) Log.d(TAG, "Missing or invalid finalCheckDigit") } } @@ -194,6 +223,27 @@ object OcrUtils { return null } + private fun cleanBelgiumDocumentNumber(doc9: String, doc3: String, checkDigit: String): String? { + // For Belgium TD1 format: IDBEL000001115<7027 + // doc9 = "000001115" (9 digits) + // doc3 = "702" (3 digits after <) + // checkDigit = "7" (single check digit) + + var cleanDoc9 = doc9 + cleanDoc9 = cleanDoc9.substring(3) + + val fullDocumentNumber = cleanDoc9 + doc3 + + val checkDigitCalculated = MRZInfo.checkDigit(fullDocumentNumber).toString().toInt() + val expectedCheckDigit = checkDigit.toInt() + + if (checkDigitCalculated == expectedCheckDigit) { + return fullDocumentNumber + } + + return null + } + private fun createDummyMrz( documentType: String, issuingState: String = "ESP", diff --git a/app/ios/LiveMRZScannerView.swift b/app/ios/LiveMRZScannerView.swift index ed6562bb1..7def9801d 100644 --- a/app/ios/LiveMRZScannerView.swift +++ b/app/ios/LiveMRZScannerView.swift @@ -80,6 +80,112 @@ struct LiveMRZScannerView: View { ] } + private func correctBelgiumDocumentNumber(result: String) -> String? { + // Belgium TD1 format: IDBEL000001115<7027 + let line1RegexPattern = "IDBEL(?[A-Z0-9]{9})<(?[A-Z0-9<]{3})(?\\d)" + guard let line1Regex = try? NSRegularExpression(pattern: line1RegexPattern) else { return nil } + let line1Matcher = line1Regex.firstMatch(in: result, options: [], range: NSRange(location: 0, length: result.count)) + + if let line1Matcher = line1Matcher { + let doc9Range = line1Matcher.range(withName: "doc9") + let doc3Range = line1Matcher.range(withName: "doc3") + let checkDigitRange = line1Matcher.range(withName: "checkDigit") + + let doc9 = (result as NSString).substring(with: doc9Range) + let doc3 = (result as NSString).substring(with: doc3Range) + let checkDigit = (result as NSString).substring(with: checkDigitRange) + + if let cleanedDoc = cleanBelgiumDocumentNumber(doc9: doc9, doc3: doc3, checkDigit: checkDigit) { + let correctedMRZLine = "IDBEL\(cleanedDoc)\(checkDigit)" + return correctedMRZLine + } + } + return nil + } + + private func cleanBelgiumDocumentNumber(doc9: String, doc3: String, checkDigit: String) -> String? { + // For Belgium TD1 format: IDBEL000001115<7027 + // doc9 = "000001115" (9 digits) + // doc3 = "702" (3 digits after <) + // checkDigit = "7" (single check digit) + + var cleanDoc9 = doc9 + // Strip first 3 characters + let startIndex = cleanDoc9.index(cleanDoc9.startIndex, offsetBy: 3) + cleanDoc9 = String(cleanDoc9[startIndex...]) + + let fullDocumentNumber = cleanDoc9 + doc3 + + + return fullDocumentNumber + } + + private func isValidMRZResult(_ result: QKMRZResult) -> Bool { + return result.isDocumentNumberValid && result.isExpiryDateValid && result.isBirthdateValid + } + + private func handleValidMRZResult(_ result: QKMRZResult) { + parsedMRZ = result + scanComplete = true + onScanComplete?(result) + onScanResultAsDict?(mapVisionResultToDictionary(result)) + } + + private func processBelgiumDocument(result: String, parser: QKMRZParser) -> QKMRZResult? { + print("[LiveMRZScannerView] Processing Belgium document") + + guard let correctedBelgiumLine = correctBelgiumDocumentNumber(result: result) else { + print("[LiveMRZScannerView] Failed to correct Belgium document number") + return nil + } + + // print("[LiveMRZScannerView] Belgium corrected line: \(correctedBelgiumLine)") + + // Split MRZ into lines and replace the first line + let lines = result.components(separatedBy: "\n") + guard lines.count >= 3 else { + print("[LiveMRZScannerView] Invalid MRZ format - not enough lines") + return nil + } + + let originalFirstLine = lines[0] + // print("[LiveMRZScannerView] Original first line: \(originalFirstLine)") + + // Pad the corrected line to 30 characters (TD1 format) + let paddedCorrectedLine = correctedBelgiumLine.padding(toLength: 30, withPad: "<", startingAt: 0) + // print("[LiveMRZScannerView] Padded corrected line: \(paddedCorrectedLine)") + + // Reconstruct the MRZ with the corrected first line + var correctedLines = lines + correctedLines[0] = paddedCorrectedLine + let correctedMRZString = correctedLines.joined(separator: "\n") + // print("[LiveMRZScannerView] Corrected MRZ string: \(correctedMRZString)") + + guard let belgiumMRZResult = parser.parse(mrzString: correctedMRZString) else { + print("[LiveMRZScannerView] Belgium MRZ result is not valid") + return nil + } + + // print("[LiveMRZScannerView] Belgium MRZ result: \(belgiumMRZResult)") + + // Try the corrected MRZ first + if isValidMRZResult(belgiumMRZResult) { + return belgiumMRZResult + } + + // If document number is still invalid, try single character correction + if !belgiumMRZResult.isDocumentNumberValid { + if let correctedResult = singleCorrectDocumentNumberInMRZ(result: correctedMRZString, docNumber: belgiumMRZResult.documentNumber, parser: parser) { + // print("[LiveMRZScannerView] Single correction successful: \(correctedResult)") + if isValidMRZResult(correctedResult) { + return correctedResult + } + } + } + + return nil + } + var body: some View { ZStack(alignment: .bottom) { CameraView( @@ -91,20 +197,31 @@ struct LiveMRZScannerView: View { // print("[LiveMRZScannerView] result: \(result)") let parser = QKMRZParser(ocrCorrection: false) if let mrzResult = parser.parse(mrzString: result) { - let doc = mrzResult; - if doc.allCheckDigitsValid == true && !scanComplete { - parsedMRZ = mrzResult - scanComplete = true - onScanComplete?(mrzResult) - onScanResultAsDict?(mapVisionResultToDictionary(mrzResult)) - } else if doc.isDocumentNumberValid == false && !scanComplete { + let doc = mrzResult + // print("[LiveMRZScannerView] doc: \(doc)") + + guard !scanComplete else { return } + + // Check if already valid + if doc.allCheckDigitsValid { + handleValidMRZResult(mrzResult) + return + } + + // Handle Belgium documents (only if not already valid) + if doc.countryCode == "BEL" { + if let belgiumResult = processBelgiumDocument(result: result, parser: parser) { + handleValidMRZResult(belgiumResult) + } + return + } + + // Handle other documents with invalid document numbers + if !doc.isDocumentNumberValid { if let correctedResult = singleCorrectDocumentNumberInMRZ(result: result, docNumber: doc.documentNumber, parser: parser) { - let correctedDoc = correctedResult - if correctedDoc.allCheckDigitsValid == true { - parsedMRZ = correctedResult - scanComplete = true - onScanComplete?(correctedResult) - onScanResultAsDict?(mapVisionResultToDictionary(correctedResult)) + // print("[LiveMRZScannerView] correctedDoc: \(correctedResult)") + if correctedResult.allCheckDigitsValid { + handleValidMRZResult(correctedResult) } } } diff --git a/packages/mobile-sdk-alpha/android/src/main/java/com/selfxyz/selfSDK/utils/OcrUtils.kt b/packages/mobile-sdk-alpha/android/src/main/java/com/selfxyz/selfSDK/utils/OcrUtils.kt index fcd9b2a5c..57cbd4571 100644 --- a/packages/mobile-sdk-alpha/android/src/main/java/com/selfxyz/selfSDK/utils/OcrUtils.kt +++ b/packages/mobile-sdk-alpha/android/src/main/java/com/selfxyz/selfSDK/utils/OcrUtils.kt @@ -26,9 +26,15 @@ object OcrUtils { private val REGEX_ID_DOCUMENT_NUMBER = "(ID)(?[A-Z<]{3})(?[A-Z0-9<]{9})(?[0-9]{1})" private val REGEX_ID_DATE_OF_BIRTH = "(?[0-9]{6})(?[0-9]{1})(?[FM<]{1})" + // Belgium TD1 (ID Card) specific pattern + private val REGEX_BELGIUM_ID_DOCUMENT_NUMBER = "IDBEL(?[A-Z0-9]{9})<(?[A-Z0-9]{3})(?\\d)" + private val REGEX_BELGIUM_ID_DATE_OF_BIRTH = "(?[0-9]{6})(?[0-9]{1})(?[FM<]{1})(?[0-9]{6})(?[0-9]{1})" + private val patternDocumentNumber = Pattern.compile(REGEX_ID_DOCUMENT_NUMBER) private val patternDateOfBirth = Pattern.compile(REGEX_ID_DATE_OF_BIRTH) private val patternDocumentCode = Pattern.compile(REGEX_ID_DOCUMENT_CODE) + private val patternBelgiumDocumentNumber = Pattern.compile(REGEX_BELGIUM_ID_DOCUMENT_NUMBER) + private val patternBelgiumDateOfBirth = Pattern.compile(REGEX_BELGIUM_ID_DATE_OF_BIRTH) fun processOcr( @@ -49,7 +55,6 @@ object OcrUtils { temp = temp.replace("\r".toRegex(), "").replace("\n".toRegex(), "").replace("\t".toRegex(), "").replace(" ", "") fullRead += "$temp-" } - // fullRead = fullRead.toUpperCase() fullRead = fullRead.uppercase() // Log.d(TAG, "Read: $fullRead") @@ -70,40 +75,63 @@ object OcrUtils { val matcherDocumentNumber = patternDocumentNumber.matcher(fullRead) val matcherDateOfBirth = patternDateOfBirth.matcher(fullRead) - val hasDocumentNumber = matcherDocumentNumber.find() val hasDateOfBirth = matcherDateOfBirth.find() + // Belgium specific matchers + val matcherBelgiumDocumentNumber = patternBelgiumDocumentNumber.matcher(fullRead) + val hasBelgiumDocumentNumber = matcherBelgiumDocumentNumber.find() + val documentNumber = if (hasDocumentNumber) matcherDocumentNumber.group("documentNumber") else null val checkDigitDocumentNumber = if (hasDocumentNumber) matcherDocumentNumber.group("checkDigitDocumentNumber")?.toIntOrNull() else null val countryCode = if (hasDocumentNumber) matcherDocumentNumber.group("country") else null val dateOfBirth = if (hasDateOfBirth) matcherDateOfBirth.group("dateOfBirth") else null + + // Belgium specific values + val belgiumCheckDigit = if (hasBelgiumDocumentNumber) matcherBelgiumDocumentNumber.group("checkDigit")?.toIntOrNull() else null + val belgiumDateOfBirth = if (hasBelgiumDocumentNumber) { + val dateOfBirthMatcher = patternBelgiumDateOfBirth.matcher(fullRead) + if (dateOfBirthMatcher.find()) dateOfBirthMatcher.group("dateOfBirth") else null + } else null + + // Final values + val finalDocumentNumber = if (hasBelgiumDocumentNumber) { + val doc9 = matcherBelgiumDocumentNumber.group("doc9") + val doc3 = matcherBelgiumDocumentNumber.group("doc3") + val checkDigit = matcherBelgiumDocumentNumber.group("checkDigit") + cleanBelgiumDocumentNumber(doc9, doc3, checkDigit) + } else documentNumber + val finalDateOfBirth = if (hasBelgiumDocumentNumber) belgiumDateOfBirth else dateOfBirth + val finalCountryCode = if (hasBelgiumDocumentNumber) "BEL" else countryCode + val finalCheckDigit = if (hasBelgiumDocumentNumber) belgiumCheckDigit else checkDigitDocumentNumber + val checkDigitDateOfBirth = if (hasDateOfBirth) matcherDateOfBirth.group("checkDigitDateOfBirth")?.toIntOrNull() else null val gender = if (hasDateOfBirth) matcherDateOfBirth.group("gender") else null - val expirationDate: String? = if (!countryCode.isNullOrEmpty()) { - val expirationDateRegex = "(?[0-9]{6})(?[0-9]{1})" + Pattern.quote(countryCode) + val expirationDate: String? = if (!finalCountryCode.isNullOrEmpty()) { + val expirationDateRegex = "(?[0-9]{6})(?[0-9]{1})" + Pattern.quote(finalCountryCode) + // val expirationDateRegex = "(?[0-9]{6})(?[0-9]{1})UTO" val patternExpirationDate = Pattern.compile(expirationDateRegex) val matcherExpirationDate = patternExpirationDate.matcher(fullRead) if (matcherExpirationDate.find()) matcherExpirationDate.group("expirationDate") else null } else null // Only proceed if all required fields are present and non-empty - if (!countryCode.isNullOrEmpty() && !documentNumber.isNullOrEmpty() && !dateOfBirth.isNullOrEmpty() && !expirationDate.isNullOrEmpty() && checkDigitDocumentNumber != null) { - val cleanDocumentNumber = cleanDocumentNumber(documentNumber, checkDigitDocumentNumber) - Log.d(TAG, "cleanDocumentNumber") + if (!finalCountryCode.isNullOrEmpty() && !finalDocumentNumber.isNullOrEmpty() && !finalDateOfBirth.isNullOrEmpty() && !expirationDate.isNullOrEmpty() && finalCheckDigit != null) { + val cleanDocumentNumber = cleanDocumentNumber(finalDocumentNumber, finalCheckDigit) + // Log.d(TAG, "cleanDocumentNumber") if (cleanDocumentNumber != null) { - val mrzInfo = createDummyMrz("ID", countryCode, cleanDocumentNumber, dateOfBirth, expirationDate) + val mrzInfo = createDummyMrz("ID", finalCountryCode, cleanDocumentNumber, finalDateOfBirth, expirationDate) // Log.d(TAG, "MRZ-TD1: $mrzInfo") callback.onMRZRead(mrzInfo, timeRequired) return } } else { - if (countryCode.isNullOrEmpty()) Log.d(TAG, "Missing or invalid countryCode") - if (documentNumber.isNullOrEmpty()) Log.d(TAG, "Missing or invalid documentNumber") - if (dateOfBirth.isNullOrEmpty()) Log.d(TAG, "Missing or invalid dateOfBirth") + if (finalCountryCode.isNullOrEmpty()) Log.d(TAG, "Missing or invalid finalCountryCode") + if (finalDocumentNumber.isNullOrEmpty()) Log.d(TAG, "Missing or invalid finalDocumentNumber") + if (finalDateOfBirth.isNullOrEmpty()) Log.d(TAG, "Missing or invalid dateOfBirth") if (expirationDate.isNullOrEmpty()) Log.d(TAG, "Missing or invalid expirationDate") - if (checkDigitDocumentNumber == null) Log.d(TAG, "Missing or invalid checkDigitDocumentNumber") + if (finalCheckDigit == null) Log.d(TAG, "Missing or invalid finalCheckDigit") } } @@ -197,6 +225,27 @@ object OcrUtils { return null } + private fun cleanBelgiumDocumentNumber(doc9: String, doc3: String, checkDigit: String): String? { + // For Belgium TD1 format: IDBEL000001115<7027 + // doc9 = "000001115" (9 digits) + // doc3 = "702" (3 digits after <) + // checkDigit = "7" (single check digit) + + var cleanDoc9 = doc9 + cleanDoc9 = cleanDoc9.substring(3) + + val fullDocumentNumber = cleanDoc9 + doc3 + + val checkDigitCalculated = MRZInfo.checkDigit(fullDocumentNumber).toString().toInt() + val expectedCheckDigit = checkDigit.toInt() + + if (checkDigitCalculated == expectedCheckDigit) { + return fullDocumentNumber + } + + return null + } + private fun createDummyMrz( documentType: String, issuingState: String = "ESP", diff --git a/packages/mobile-sdk-alpha/ios/SelfSDK/SelfLiveMRZScannerView.swift b/packages/mobile-sdk-alpha/ios/SelfSDK/SelfLiveMRZScannerView.swift index 13326e01e..f5b1c6298 100644 --- a/packages/mobile-sdk-alpha/ios/SelfSDK/SelfLiveMRZScannerView.swift +++ b/packages/mobile-sdk-alpha/ios/SelfSDK/SelfLiveMRZScannerView.swift @@ -83,6 +83,112 @@ struct SelfLiveMRZScannerView: View { ] } + private func correctBelgiumDocumentNumber(result: String) -> String? { + // Belgium TD1 format: IDBEL000001115<7027 + let line1RegexPattern = "IDBEL(?[A-Z0-9]{9})<(?[A-Z0-9<]{3})(?\\d)" + guard let line1Regex = try? NSRegularExpression(pattern: line1RegexPattern) else { return nil } + let line1Matcher = line1Regex.firstMatch(in: result, options: [], range: NSRange(location: 0, length: result.count)) + + if let line1Matcher = line1Matcher { + let doc9Range = line1Matcher.range(withName: "doc9") + let doc3Range = line1Matcher.range(withName: "doc3") + let checkDigitRange = line1Matcher.range(withName: "checkDigit") + + let doc9 = (result as NSString).substring(with: doc9Range) + let doc3 = (result as NSString).substring(with: doc3Range) + let checkDigit = (result as NSString).substring(with: checkDigitRange) + + if let cleanedDoc = cleanBelgiumDocumentNumber(doc9: doc9, doc3: doc3, checkDigit: checkDigit) { + let correctedMRZLine = "IDBEL\(cleanedDoc)\(checkDigit)" + return correctedMRZLine + } + } + return nil + } + + private func cleanBelgiumDocumentNumber(doc9: String, doc3: String, checkDigit: String) -> String? { + // For Belgium TD1 format: IDBEL000001115<7027 + // doc9 = "000001115" (9 digits) + // doc3 = "702" (3 digits after <) + // checkDigit = "7" (single check digit) + + var cleanDoc9 = doc9 + // Strip first 3 characters + let startIndex = cleanDoc9.index(cleanDoc9.startIndex, offsetBy: 3) + cleanDoc9 = String(cleanDoc9[startIndex...]) + + let fullDocumentNumber = cleanDoc9 + doc3 + + + return fullDocumentNumber + } + + private func isValidMRZResult(_ result: QKMRZResult) -> Bool { + return result.isDocumentNumberValid && result.isExpiryDateValid && result.isBirthdateValid + } + + private func handleValidMRZResult(_ result: QKMRZResult) { + parsedMRZ = result + scanComplete = true + onScanComplete?(result) + onScanResultAsDict?(mapVisionResultToDictionary(result)) + } + + private func processBelgiumDocument(result: String, parser: QKMRZParser) -> QKMRZResult? { + print("[LiveMRZScannerView] Processing Belgium document") + + guard let correctedBelgiumLine = correctBelgiumDocumentNumber(result: result) else { + print("[LiveMRZScannerView] Failed to correct Belgium document number") + return nil + } + + // print("[LiveMRZScannerView] Belgium corrected line: \(correctedBelgiumLine)") + + // Split MRZ into lines and replace the first line + let lines = result.components(separatedBy: "\n") + guard lines.count >= 3 else { + print("[LiveMRZScannerView] Invalid MRZ format - not enough lines") + return nil + } + + let originalFirstLine = lines[0] + // print("[LiveMRZScannerView] Original first line: \(originalFirstLine)") + + // Pad the corrected line to 30 characters (TD1 format) + let paddedCorrectedLine = correctedBelgiumLine.padding(toLength: 30, withPad: "<", startingAt: 0) + // print("[LiveMRZScannerView] Padded corrected line: \(paddedCorrectedLine)") + + // Reconstruct the MRZ with the corrected first line + var correctedLines = lines + correctedLines[0] = paddedCorrectedLine + let correctedMRZString = correctedLines.joined(separator: "\n") + // print("[LiveMRZScannerView] Corrected MRZ string: \(correctedMRZString)") + + guard let belgiumMRZResult = parser.parse(mrzString: correctedMRZString) else { + print("[LiveMRZScannerView] Belgium MRZ result is not valid") + return nil + } + + // print("[LiveMRZScannerView] Belgium MRZ result: \(belgiumMRZResult)") + + // Try the corrected MRZ first + if isValidMRZResult(belgiumMRZResult) { + return belgiumMRZResult + } + + // If document number is still invalid, try single character correction + if !belgiumMRZResult.isDocumentNumberValid { + if let correctedResult = singleCorrectDocumentNumberInMRZ(result: correctedMRZString, docNumber: belgiumMRZResult.documentNumber, parser: parser) { + // print("[LiveMRZScannerView] Single correction successful: \(correctedResult)") + if isValidMRZResult(correctedResult) { + return correctedResult + } + } + } + + return nil + } + var body: some View { ZStack(alignment: .bottom) { SelfCameraView( @@ -94,22 +200,31 @@ struct SelfLiveMRZScannerView: View { // print("[LiveMRZScannerView] result: \(result)") let parser = QKMRZParser(ocrCorrection: false) if let mrzResult = parser.parse(mrzString: result) { - let doc = mrzResult; + let doc = mrzResult // print("[LiveMRZScannerView] doc: \(doc)") - if doc.allCheckDigitsValid == true && !scanComplete { - parsedMRZ = mrzResult - scanComplete = true - onScanComplete?(mrzResult) - onScanResultAsDict?(mapVisionResultToDictionary(mrzResult)) - } else if doc.isDocumentNumberValid == false && !scanComplete { + + guard !scanComplete else { return } + + // Check if already valid + if doc.allCheckDigitsValid { + handleValidMRZResult(mrzResult) + return + } + + // Handle Belgium documents (only if not already valid) + if doc.countryCode == "BEL" { + if let belgiumResult = processBelgiumDocument(result: result, parser: parser) { + handleValidMRZResult(belgiumResult) + } + return + } + + // Handle other documents with invalid document numbers + if !doc.isDocumentNumberValid { if let correctedResult = singleCorrectDocumentNumberInMRZ(result: result, docNumber: doc.documentNumber, parser: parser) { - let correctedDoc = correctedResult - // print("[LiveMRZScannerView] correctedDoc: \(correctedDoc)") - if correctedDoc.allCheckDigitsValid == true { - parsedMRZ = correctedResult - scanComplete = true - onScanComplete?(correctedResult) - onScanResultAsDict?(mapVisionResultToDictionary(correctedResult)) + // print("[LiveMRZScannerView] correctedDoc: \(correctedResult)") + if correctedResult.allCheckDigitsValid { + handleValidMRZResult(correctedResult) } } } From f7e9c40a5d9603d83344f34924abc676a473220f Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Fri, 12 Sep 2025 14:03:18 -0700 Subject: [PATCH 11/25] fix: OFAC trees not found (#1060) * fix: relax OFAC tree response validation * test: cover OFAC tree edge cases * fix stateless * revert and fix types * fix tests --- app/src/utils/proving/provingMachine.ts | 25 +- .../provingMachine.disclose.stateless.test.ts | 342 ++++++++++++++++++ common/src/utils/ofac.test.ts | 73 ++++ common/src/utils/ofac.ts | 17 +- .../generate-disclosure-inputs.test.ts | 13 + .../tests/stores/protocolStore.test.ts | 76 ++++ 6 files changed, 537 insertions(+), 9 deletions(-) create mode 100644 app/tests/utils/proving/provingMachine.disclose.stateless.test.ts create mode 100644 common/src/utils/ofac.test.ts create mode 100644 packages/mobile-sdk-alpha/tests/stores/protocolStore.test.ts diff --git a/app/src/utils/proving/provingMachine.ts b/app/src/utils/proving/provingMachine.ts index 463678f22..6d1c41102 100644 --- a/app/src/utils/proving/provingMachine.ts +++ b/app/src/utils/proving/provingMachine.ts @@ -18,6 +18,7 @@ import { } from '@selfxyz/common/utils'; import { getPublicKey, verifyAttestation } from '@selfxyz/common/utils/attest'; import { + generateTEEInputsDiscloseStateless, generateTEEInputsDSC, generateTEEInputsRegister, } from '@selfxyz/common/utils/circuits/registerInputs'; @@ -38,7 +39,6 @@ import { } from '@selfxyz/common/utils/proving'; import { clearPassportData, - generateTEEInputsDisclose, hasAnyValidRegisteredDocument, loadSelectedDocument, markCurrentDocumentAsRegistered, @@ -459,7 +459,7 @@ export const useProvingStore = create((set, get) => { selfClient.emit(SdkEvents.PROVING_REGISTER_ERROR_OR_FAILURE, { hasValidDocument: hasValid, }); - } catch (error) { + } catch { selfClient.emit(SdkEvents.PROVING_REGISTER_ERROR_OR_FAILURE, { hasValidDocument: false, }); @@ -1015,7 +1015,7 @@ export const useProvingStore = create((set, get) => { } }, - _closeConnections: (selfClient: SelfClient) => { + _closeConnections: (_selfClient: SelfClient) => { const { wsConnection: ws, wsHandlers } = get(); if (ws && wsHandlers) { try { @@ -1088,10 +1088,27 @@ export const useProvingStore = create((set, get) => { break; case 'disclose': ({ inputs, circuitName, endpointType, endpoint } = - generateTEEInputsDisclose( + generateTEEInputsDiscloseStateless( secret as string, passportData, selfApp as SelfApp, + (doc: DocumentCategory, tree) => { + const docStore = + doc === 'passport' + ? protocolStore.passport + : protocolStore.id_card; + switch (tree) { + case 'ofac': + return docStore.ofac_trees; + case 'commitment': + if (!docStore.commitment_tree) { + throw new Error('Commitment tree not loaded'); + } + return docStore.commitment_tree; + default: + throw new Error('Unknown tree type'); + } + }, )); circuitTypeWithDocumentExtension = `disclose`; break; diff --git a/app/tests/utils/proving/provingMachine.disclose.stateless.test.ts b/app/tests/utils/proving/provingMachine.disclose.stateless.test.ts new file mode 100644 index 000000000..8562fd5a8 --- /dev/null +++ b/app/tests/utils/proving/provingMachine.disclose.stateless.test.ts @@ -0,0 +1,342 @@ +// 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 { SelfClient } from '@selfxyz/mobile-sdk-alpha'; +import { + useProtocolStore, + useSelfAppStore, +} from '@selfxyz/mobile-sdk-alpha/stores'; + +// Do not import provingMachine here; we'll require it after setting up mocks per test + +jest.mock('xstate', () => { + const actual = jest.requireActual('xstate') as any; + const { actorMock } = require('./actorMock'); + return { ...actual, createActor: jest.fn(() => actorMock) }; +}); + +// Mock proving utils for payload building +jest.mock('@selfxyz/common/utils/proving', () => { + const actual = jest.requireActual('@selfxyz/common/utils/proving') as any; + return { + ...actual, + getPayload: jest.fn(() => ({ mocked: true })), + encryptAES256GCM: jest.fn(() => ({ + nonce: [0], + cipher_text: [1], + auth_tag: [2], + })), + }; +}); + +describe('_generatePayload disclose (stateless resolver)', () => { + const selfClient: SelfClient = { + trackEvent: jest.fn(), + } as unknown as SelfClient; + + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + + useSelfAppStore.setState({ + selfApp: { + chainID: 42220, + userId: '12345678-1234-1234-1234-123456789abc', + userDefinedData: '0x0', + endpointType: 'https', + endpoint: 'https://endpoint', + scope: 'scope', + sessionId: '', + appName: '', + logoBase64: '', + header: '', + userIdType: 'uuid', + devMode: false, + disclosures: {}, + version: 1, + deeplinkCallback: '', + }, + }); + }); + + it('uses resolver to fetch ofac and commitment trees', async () => { + // Mock the stateless generator to assert resolver behavior + const genMock = jest.fn((secret, passportData, selfApp, getTree) => { + const ofac = getTree('passport', 'ofac'); + const commit = getTree('passport', 'commitment'); + expect(ofac).toEqual({ + passportNoAndNationality: { root: ['pp'] }, + nameAndDob: { root: ['dob'] }, + nameAndYob: { root: ['yob'] }, + }); + expect(commit).toBe('[[]]'); + return { + inputs: { s: 1 }, + circuitName: 'vc_and_disclose', + endpointType: 'https', + endpoint: 'https://dis', + }; + }); + jest.doMock('@selfxyz/common/utils/circuits/registerInputs', () => ({ + generateTEEInputsDiscloseStateless: genMock, + generateTEEInputsRegister: jest.fn(), + generateTEEInputsDSC: jest.fn(), + })); + + // Act (reload module after doMock) + let store: any; + let protocolStore: any; + jest.isolateModules(() => { + // require after mocks are in place + const mod = require('@/utils/proving/provingMachine'); + const { + useProtocolStore: isolatedProtocolStore, + } = require('@selfxyz/mobile-sdk-alpha/stores'); + store = mod.useProvingStore; + protocolStore = isolatedProtocolStore; + + // Set protocol store state inside isolateModules + protocolStore.setState({ + passport: { + dsc_tree: 'tree', + csca_tree: [[new Uint8Array([1])]], + commitment_tree: '[[]]', + deployed_circuits: null, + circuits_dns_mapping: null, + alternative_csca: {}, + ofac_trees: { + passportNoAndNationality: { root: ['pp'] }, + nameAndDob: { root: ['dob'] }, + nameAndYob: { root: ['yob'] }, + }, + fetch_deployed_circuits: jest.fn(), + fetch_circuits_dns_mapping: jest.fn(), + fetch_csca_tree: jest.fn(), + fetch_dsc_tree: jest.fn(), + fetch_identity_tree: jest.fn(), + fetch_alternative_csca: jest.fn(), + fetch_ofac_trees: jest.fn(), + fetch_all: jest.fn(), + }, + id_card: { + commitment_tree: null, + dsc_tree: null, + csca_tree: null, + deployed_circuits: null, + circuits_dns_mapping: null, + alternative_csca: {}, + ofac_trees: null, + fetch_deployed_circuits: jest.fn(), + fetch_circuits_dns_mapping: jest.fn(), + fetch_csca_tree: jest.fn(), + fetch_dsc_tree: jest.fn(), + fetch_identity_tree: jest.fn(), + fetch_alternative_csca: jest.fn(), + fetch_ofac_trees: jest.fn(), + fetch_all: jest.fn(), + }, + } as any); + + // Set proving store state inside isolateModules so it affects the isolated store instance + store.setState({ + circuitType: 'disclose', + passportData: { + documentCategory: 'passport', + mock: false, + dsc_parsed: { authorityKeyIdentifier: 'abcd' }, + passportMetadata: { + signatureAlgorithm: 'rsa_pss_rsae_sha256', + signedAttrHashFunction: 'sha256', + issuer: 'X', + validFrom: new Date('2020-01-01'), + validTo: new Date('2030-01-01'), + }, + mrz: 'P { + const genMock = jest.fn((secret, passportData, selfApp, getTree) => { + // This should throw inside resolver when requesting commitment + getTree('passport', 'commitment'); + return { + inputs: {}, + circuitName: '', + endpointType: 'https', + endpoint: '', + }; + }); + jest.doMock('@selfxyz/common/utils/circuits/registerInputs', () => ({ + generateTEEInputsDiscloseStateless: genMock, + generateTEEInputsRegister: jest.fn(), + generateTEEInputsDSC: jest.fn(), + })); + + let store: any; + let protocolStore: any; + jest.isolateModules(() => { + const mod = require('@/utils/proving/provingMachine'); + const { + useProtocolStore: isolatedProtocolStore, + } = require('@selfxyz/mobile-sdk-alpha/stores'); + store = mod.useProvingStore; + protocolStore = isolatedProtocolStore; + + // Set protocol store state inside isolateModules - missing commitment tree + protocolStore.setState({ + passport: { + dsc_tree: 'tree', + csca_tree: [[new Uint8Array([1])]], + commitment_tree: null, + deployed_circuits: null, + circuits_dns_mapping: null, + alternative_csca: {}, + ofac_trees: { + passportNoAndNationality: { root: ['pp'] }, + nameAndDob: { root: ['dob'] }, + nameAndYob: { root: ['yob'] }, + }, + fetch_deployed_circuits: jest.fn(), + fetch_circuits_dns_mapping: jest.fn(), + fetch_csca_tree: jest.fn(), + fetch_dsc_tree: jest.fn(), + fetch_identity_tree: jest.fn(), + fetch_alternative_csca: jest.fn(), + fetch_ofac_trees: jest.fn(), + fetch_all: jest.fn(), + }, + id_card: {} as any, + } as any); + + // Set store state inside isolateModules so it affects the isolated store instance + store.setState({ + circuitType: 'disclose', + passportData: { + documentCategory: 'passport', + mock: false, + dsc_parsed: { authorityKeyIdentifier: 'abcd' }, + passportMetadata: { + signatureAlgorithm: 'rsa_pss_rsae_sha256', + signedAttrHashFunction: 'sha256', + issuer: 'X', + validFrom: new Date('2020-01-01'), + validTo: new Date('2030-01-01'), + }, + mrz: 'P { + const genMock = jest.fn((secret, passportData, selfApp, getTree) => { + const ofac = getTree('passport', 'ofac'); + if (!ofac) { + throw new Error('OFAC trees not loaded'); + } + return { + inputs: {}, + circuitName: '', + endpointType: 'https', + endpoint: '', + }; + }); + jest.doMock('@selfxyz/common/utils/circuits/registerInputs', () => ({ + generateTEEInputsDiscloseStateless: genMock, + generateTEEInputsRegister: jest.fn(), + generateTEEInputsDSC: jest.fn(), + })); + + let store: any; + let protocolStore: any; + jest.isolateModules(() => { + const mod = require('@/utils/proving/provingMachine'); + const { + useProtocolStore: isolatedProtocolStore, + } = require('@selfxyz/mobile-sdk-alpha/stores'); + store = mod.useProvingStore; + protocolStore = isolatedProtocolStore; + + // Set protocol store state inside isolateModules - missing OFAC trees + protocolStore.setState({ + passport: { + dsc_tree: 'tree', + csca_tree: [[new Uint8Array([1])]], + commitment_tree: '[[]]', + deployed_circuits: null, + circuits_dns_mapping: null, + alternative_csca: {}, + ofac_trees: null, + fetch_deployed_circuits: jest.fn(), + fetch_circuits_dns_mapping: jest.fn(), + fetch_csca_tree: jest.fn(), + fetch_dsc_tree: jest.fn(), + fetch_identity_tree: jest.fn(), + fetch_alternative_csca: jest.fn(), + fetch_ofac_trees: jest.fn(), + fetch_all: jest.fn(), + }, + id_card: {} as any, + } as any); + + // Set store state inside isolateModules so it affects the isolated store instance + store.setState({ + circuitType: 'disclose', + passportData: { + documentCategory: 'passport', + mock: false, + dsc_parsed: { authorityKeyIdentifier: 'abcd' }, + passportMetadata: { + signatureAlgorithm: 'rsa_pss_rsae_sha256', + signedAttrHashFunction: 'sha256', + issuer: 'X', + validFrom: new Date('2020-01-01'), + validTo: new Date('2030-01-01'), + }, + mrz: 'P { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + it('accepts raw tree payloads', async () => { + const responses: Record = { + 'passport-no-nationality': { root: ['pp'] }, + 'name-dob': { root: ['dob'] }, + 'name-yob': { root: ['yob'] }, + }; + + vi.spyOn(global, 'fetch').mockImplementation( + (input: string | Request | URL, _init?: RequestInit) => { + const url = typeof input === 'string' ? input : input.toString(); + const key = url.includes('passport-no-nationality') + ? 'passport-no-nationality' + : url.includes('name-dob') + ? 'name-dob' + : 'name-yob'; + return Promise.resolve({ ok: true, json: async () => responses[key] } as Response); + } + ); + + const trees = await fetchOfacTrees('prod', 'passport'); + expect(trees).toEqual({ + passportNoAndNationality: responses['passport-no-nationality'], + nameAndDob: responses['name-dob'], + nameAndYob: responses['name-yob'], + }); + }); + + it('accepts wrapped {status, data} payloads', async () => { + const responses: Record = { + 'passport-no-nationality': { status: 'success', data: { root: ['pp'] } }, + 'name-dob': { status: 'success', data: { root: ['dob'] } }, + 'name-yob': { status: 'success', data: { root: ['yob'] } }, + }; + + vi.spyOn(global, 'fetch').mockImplementation( + (input: string | Request | URL, _init?: RequestInit) => { + const url = typeof input === 'string' ? input : input.toString(); + const key = url.includes('passport-no-nationality') + ? 'passport-no-nationality' + : url.includes('name-dob') + ? 'name-dob' + : 'name-yob'; + return Promise.resolve({ ok: true, json: async () => responses[key] } as Response); + } + ); + + const trees = await fetchOfacTrees('prod', 'passport'); + expect(trees).toEqual({ + passportNoAndNationality: responses['passport-no-nationality'].data, + nameAndDob: responses['name-dob'].data, + nameAndYob: responses['name-yob'].data, + }); + }); +}); diff --git a/common/src/utils/ofac.ts b/common/src/utils/ofac.ts index c8e6a6706..ee67b3409 100644 --- a/common/src/utils/ofac.ts +++ b/common/src/utils/ofac.ts @@ -14,12 +14,19 @@ const fetchTree = async (url: string): Promise => { throw new Error(`HTTP error fetching ${url}! status: ${res.status}`); } const responseData = await res.json(); - if (responseData.status !== 'success' || !responseData.data) { - throw new Error( - `Failed to fetch tree from ${url}: ${responseData.message || 'Invalid response format'}` - ); + + // Handle wrapped responses with {status: 'success', data: ...} format + if (responseData && typeof responseData === 'object' && 'status' in responseData) { + if (responseData.status !== 'success' || !responseData.data) { + throw new Error( + `Failed to fetch tree from ${url}: ${responseData.message || 'Invalid response format'}` + ); + } + return responseData.data; } - return responseData.data; + + // Handle raw responses (direct tree data) + return responseData; }; // Main public helper that retrieves the three OFAC trees depending on the variant (passport vs id_card). diff --git a/packages/mobile-sdk-alpha/tests/processing/generate-disclosure-inputs.test.ts b/packages/mobile-sdk-alpha/tests/processing/generate-disclosure-inputs.test.ts index c2c7c24b0..754702efb 100644 --- a/packages/mobile-sdk-alpha/tests/processing/generate-disclosure-inputs.test.ts +++ b/packages/mobile-sdk-alpha/tests/processing/generate-disclosure-inputs.test.ts @@ -145,4 +145,17 @@ describe('generateTEEInputsDisclose', () => { `Invalid OFAC tree structure: missing required fields`, ); }); + + it('throws error if OFAC trees not loaded', () => { + vi.spyOn(useProtocolStore, 'getState').mockReturnValue({ + passport: { + ofac_trees: null, + commitment_tree: '[[]]', + }, + } as any); + + expect(() => generateTEEInputsDisclose(mockSecret, mockPassportData, mockSelfApp)).toThrowError( + 'OFAC trees not loaded', + ); + }); }); diff --git a/packages/mobile-sdk-alpha/tests/stores/protocolStore.test.ts b/packages/mobile-sdk-alpha/tests/stores/protocolStore.test.ts new file mode 100644 index 000000000..8aae2a15c --- /dev/null +++ b/packages/mobile-sdk-alpha/tests/stores/protocolStore.test.ts @@ -0,0 +1,76 @@ +// 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. + +/** + * @vitest-environment node + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { useProtocolStore } from '../../src/stores/protocolStore'; + +const originalFetch = global.fetch; + +describe('protocolStore.fetch_ofac_trees', () => { + beforeEach(() => { + useProtocolStore.setState(state => ({ + passport: { ...state.passport, ofac_trees: null }, + })); + }); + + afterEach(() => { + global.fetch = originalFetch; + vi.restoreAllMocks(); + }); + + it('stores OFAC trees when responses are raw payloads', async () => { + const responses: Record = { + 'passport-no-nationality': { root: ['pp'] }, + 'name-dob': { root: ['dob'] }, + 'name-yob': { root: ['yob'] }, + }; + + vi.spyOn(global, 'fetch').mockImplementation((input: string | Request | URL, _init?: RequestInit) => { + const url = typeof input === 'string' ? input : input.toString(); + const key = url.includes('passport-no-nationality') + ? 'passport-no-nationality' + : url.includes('name-dob') + ? 'name-dob' + : 'name-yob'; + return Promise.resolve({ ok: true, json: async () => responses[key] } as Response); + }); + + await useProtocolStore.getState().passport.fetch_ofac_trees('prod'); + expect(useProtocolStore.getState().passport.ofac_trees).toEqual({ + passportNoAndNationality: responses['passport-no-nationality'], + nameAndDob: responses['name-dob'], + nameAndYob: responses['name-yob'], + }); + }); + + it('stores OFAC trees when responses are wrapped payloads', async () => { + const responses: Record = { + 'passport-no-nationality': { status: 'success', data: { root: ['pp'] } }, + 'name-dob': { status: 'success', data: { root: ['dob'] } }, + 'name-yob': { status: 'success', data: { root: ['yob'] } }, + }; + + vi.spyOn(global, 'fetch').mockImplementation((input: string | Request | URL, _init?: RequestInit) => { + const url = typeof input === 'string' ? input : input.toString(); + const key = url.includes('passport-no-nationality') + ? 'passport-no-nationality' + : url.includes('name-dob') + ? 'name-dob' + : 'name-yob'; + return Promise.resolve({ ok: true, json: async () => responses[key] } as Response); + }); + + await useProtocolStore.getState().passport.fetch_ofac_trees('prod'); + expect(useProtocolStore.getState().passport.ofac_trees).toEqual({ + passportNoAndNationality: responses['passport-no-nationality'].data, + nameAndDob: responses['name-dob'].data, + nameAndYob: responses['name-yob'].data, + }); + }); +}); From 9ff1ae9c7d1cc955a13bf9152a31820de95548c5 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Fri, 12 Sep 2025 17:12:44 -0700 Subject: [PATCH 12/25] [SELF-723] feat: add structured NFC and Proof logging (#1048) * feat: add structured NFC logging * fix ci * Fix: add deps * logging fixes. use breadcrumbs * fix android build * update SeverityLevel * [SELF-705] feat: add proof event logging (#1057) * feat: add proof event logging * refactor: unify sentry event logging * fix types * fix mock * simplify * code rabbit feedback * fix tests --------- Co-authored-by: seshanthS --- app/android/app/build.gradle | 2 + .../android/build.gradle | 2 + .../io/tradle/nfc/RNPassportReaderModule.kt | 126 ++-- app/ios/PassportReader.m | 1 + app/ios/PassportReader.swift | 42 +- app/ios/Self.xcodeproj/project.pbxproj | 20 +- app/src/Sentry.ts | 157 ++++ app/src/Sentry.web.ts | 157 ++++ .../document/DocumentNFCScanScreen.tsx | 88 +++ .../types/react-native-passport-reader.d.ts | 2 + app/src/utils/nfcScanner.ts | 50 +- app/src/utils/passportReader.ts | 3 + app/src/utils/proving/provingMachine.ts | 712 +++++++++++++----- app/src/utils/proving/statusHandlers.ts | 108 +++ app/tests/utils/nfcScanner.test.ts | 5 + app/tests/utils/proving/actorMock.ts | 16 +- .../provingMachine.integration.test.ts | 277 +++++++ .../utils/proving/statusHandlers.test.ts | 174 +++++ .../src/utils/aadhaar/assets/dataInput.d.ts | 3 + 19 files changed, 1698 insertions(+), 247 deletions(-) create mode 100644 app/src/utils/proving/statusHandlers.ts create mode 100644 app/tests/utils/proving/provingMachine.integration.test.ts create mode 100644 app/tests/utils/proving/statusHandlers.test.ts create mode 100644 common/src/utils/aadhaar/assets/dataInput.d.ts diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index 73e35a8ac..8eba20f82 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -3,6 +3,8 @@ apply plugin: "com.facebook.react" apply plugin: "org.jetbrains.kotlin.android" apply plugin: 'kotlin-android' apply plugin: 'com.google.gms.google-services' +apply from: "../../../node_modules/@sentry/react-native/sentry.gradle" + /** diff --git a/app/android/react-native-passport-reader/android/build.gradle b/app/android/react-native-passport-reader/android/build.gradle index c0963bbe3..8c41111aa 100644 --- a/app/android/react-native-passport-reader/android/build.gradle +++ b/app/android/react-native-passport-reader/android/build.gradle @@ -42,4 +42,6 @@ dependencies { implementation 'commons-io:commons-io:2.8.0' implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation 'com.facebook.react:react-native:+' + implementation "io.sentry:sentry-android:8.20.0" + } diff --git a/app/android/react-native-passport-reader/android/src/main/java/io/tradle/nfc/RNPassportReaderModule.kt b/app/android/react-native-passport-reader/android/src/main/java/io/tradle/nfc/RNPassportReaderModule.kt index bd21da17f..6a841a938 100644 --- a/app/android/react-native-passport-reader/android/src/main/java/io/tradle/nfc/RNPassportReaderModule.kt +++ b/app/android/react-native-passport-reader/android/src/main/java/io/tradle/nfc/RNPassportReaderModule.kt @@ -115,10 +115,13 @@ import com.facebook.react.bridge.Arguments import com.facebook.react.modules.core.DeviceEventManagerModule import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Callback +import io.sentry.Breadcrumb +import io.sentry.Sentry +import io.sentry.SentryLevel object Messages { const val SCANNING = "Scanning....." - const val STOP_MOVING = "Stop moving....." + const val STOP_MOVING = "Stop moving....." const val AUTH = "Auth....." const val COMPARING = "Comparing....." const val COMPLETED = "Scanning completed" @@ -159,7 +162,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) private var opts: ReadableMap? = null private val apduLogger = APDULogger() private var currentSessionId: String? = null - + data class Data(val id: String, val digest: String, val signature: String, val publicKey: String) data class PassportData( @@ -171,7 +174,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) interface DataCallback { fun onDataReceived(data: String) } - + init { instance = this reactContext.addLifecycleEventListener(this) @@ -200,10 +203,10 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) @ReactMethod fun scan(opts: ReadableMap, promise: Promise) { - currentSessionId = generateSessionId() - + currentSessionId = if (opts.hasKey("sessionId")) opts.getString("sessionId") else generateSessionId() + apduLogger.setContext("session_id", currentSessionId!!) - + // Log scan start logAnalyticsEvent("nfc_scan_started", mapOf( "use_can" to (opts.getBoolean(PARAM_USE_CAN) ?: false), @@ -211,18 +214,22 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) "has_can_number" to (!opts.getString(PARAM_CAN).isNullOrEmpty()), "platform" to "android" )) - + + logNfc(SentryLevel.INFO, "scan_start", "start") + eventMessageEmitter(Messages.SCANNING) val mNfcAdapter = NfcAdapter.getDefaultAdapter(reactApplicationContext) // val mNfcAdapter = NfcAdapter.getDefaultAdapter(this.reactContext) if (mNfcAdapter == null) { logAnalyticsError("nfc_not_supported", "NFC chip reading not supported") + logNfc(SentryLevel.WARNING, "nfc_not_supported", "check") promise.reject("E_NOT_SUPPORTED", "NFC chip reading not supported") return } if (!mNfcAdapter.isEnabled) { logAnalyticsError("nfc_not_enabled", "NFC chip reading not enabled") + logNfc(SentryLevel.WARNING, "nfc_not_enabled", "check") promise.reject("E_NOT_ENABLED", "NFC chip reading not enabled") return } @@ -290,7 +297,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) } } - + private fun toBase64(bitmap: Bitmap, quality: Int): String { val byteArrayOutputStream = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.JPEG, quality, byteArrayOutputStream) @@ -348,9 +355,9 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) false, ) Log.e("MY_LOGS", "service gotten") - + service.addAPDUListener(apduLogger) - + service.open() Log.e("MY_LOGS", "service opened") logAnalyticsEvent("nfc_passport_service_opened") @@ -368,7 +375,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) eventMessageEmitter(Messages.PACE_STARTED) apduLogger.setContext("operation", "pace_authentication") apduLogger.setContext("auth_key_type", authKey.javaClass.simpleName) - + // Determine proper PACE key: use CAN key if provided; otherwise derive PACE MRZ key from BAC val paceKeyToUse: PACEKeySpec? = when (authKey) { is PACEKeySpec -> authKey @@ -410,10 +417,10 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) val maxAttempts = 3 eventMessageEmitter(Messages.BAC_STARTED) - + apduLogger.setContext("operation", "bac_authentication") apduLogger.setContext("auth_key_type", authKey.javaClass.simpleName) - + while (!bacSucceeded && attempts < maxAttempts) { try { attempts++ @@ -486,11 +493,11 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) logAnalyticsEvent("nfc_reading_data_groups") - + apduLogger.setContext("operation", "reading_data_groups") apduLogger.setContext("pace_succeeded", paceSucceeded) apduLogger.setContext("bac_succeeded", bacSucceeded) - + eventMessageEmitter(Messages.READING_DG1) logAnalyticsEvent("nfc_reading_dg1_started") val dg1In = service.getInputStream(PassportService.EF_DG1) @@ -570,7 +577,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) private fun doChipAuth(service: PassportService) { try { apduLogger.setContext("operation", "chip_authentication") - + logAnalyticsEvent("nfc_reading_dg14_started") eventMessageEmitter(Messages.READING_DG14) val dg14In = service.getInputStream(PassportService.EF_DG14) @@ -602,19 +609,19 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) try { apduLogger.setContext("operation", "passive_authentication") apduLogger.setContext("chip_auth_succeeded", chipAuthSucceeded) - + logAnalyticsEvent("nfc_passive_auth_started") Log.d(TAG, "Starting passive authentication...") val digest = MessageDigest.getInstance(sodFile.digestAlgorithm) Log.d(TAG, "Using digest algorithm: ${sodFile.digestAlgorithm}") - + val dataHashes = sodFile.dataGroupHashes - + val dg14Hash = if (chipAuthSucceeded) digest.digest(dg14Encoded) else ByteArray(0) val dg1Hash = digest.digest(dg1File.encoded) val dg2Hash = digest.digest(dg2File.encoded) - + // val gson = Gson() // Log.d(TAG, "dataHashes " + gson.toJson(dataHashes)) // val hexMap = sodFile.dataGroupHashes.mapValues { (_, value) -> @@ -741,7 +748,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) } apduLogger.clearContext() - + resetState() return } @@ -757,12 +764,12 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) // val signedDataField = SODFile::class.java.getDeclaredField("signedData") // signedDataField.isAccessible = true - + // val signedData = signedDataField.get(sodFile) as SignedData - + val eContentAsn1InputStream = ASN1InputStream(sodFile.eContent.inputStream()) // val eContentDecomposed: ASN1Primitive = eContentAsn1InputStream.readObject() - + val passport = Arguments.createMap() passport.putString("mrz", mrzInfo.toString()) passport.putString("signatureAlgorithm", sodFile.docSigningCertificate.sigAlgName) // this one is new @@ -772,7 +779,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) val certificateBytes = certificate.encoded val certificateBase64 = Base64.encodeToString(certificateBytes, Base64.DEFAULT) Log.d(TAG, "certificateBase64: ${certificateBase64}") - + passport.putString("documentSigningCertificate", certificateBase64) @@ -781,10 +788,10 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) passport.putString("modulus", publicKey.modulus.toString()) } else if (publicKey is ECPublicKey) { // Handle the elliptic curve public key case - + // val w = publicKey.getW() // passport.putString("publicKeyW", w.toString()) - + // val ecParams = publicKey.getParams() // passport.putInt("cofactor", ecParams.getCofactor()) // passport.putString("curve", ecParams.getCurve().toString()) @@ -793,7 +800,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) // if (ecParams is ECNamedCurveSpec) { // passport.putString("curveName", ecParams.getName()) // } - + // Old one, probably wrong: // passport.putString("curveName", (publicKey.parameters as ECNamedCurveSpec).name) // passport.putString("curveName", (publicKey.parameters.algorithm)) or maybe this @@ -831,15 +838,15 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) // passport.putString("getDocSigningCertificate", gson.toJson(sodFile.getDocSigningCertificate)) // passport.putString("getIssuerX500Principal", gson.toJson(sodFile.getIssuerX500Principal)) // passport.putString("getSerialNumber", gson.toJson(sodFile.getSerialNumber)) - - - // Another way to get signing time is to get into signedData.signerInfos, then search for the ICO identifier 1.2.840.113549.1.9.5 + + + // Another way to get signing time is to get into signedData.signerInfos, then search for the ICO identifier 1.2.840.113549.1.9.5 // passport.putString("signerInfos", gson.toJson(signedData.signerInfos)) - + // Log.d(TAG, "signedData.digestAlgorithms: ${gson.toJson(signedData.digestAlgorithms)}") // Log.d(TAG, "signedData.signerInfos: ${gson.toJson(signedData.signerInfos)}") // Log.d(TAG, "signedData.certificates: ${gson.toJson(signedData.certificates)}") - + // var quality = 100 // val base64 = bitmap?.let { toBase64(it, quality) } // val photo = Arguments.createMap() @@ -848,13 +855,13 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) // photo.putInt("height", bitmap?.height ?: 0) // passport.putMap("photo", photo) // passport.putString("dg2File", gson.toJson(dg2File)) - + eventMessageEmitter(Messages.COMPLETED) scanPromise?.resolve(passport) eventMessageEmitter(Messages.RESET) - + apduLogger.clearContext() - + resetState() } } @@ -890,10 +897,10 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) if (params.isNotEmpty()) { logData.put("data", JSONObject(Gson().toJson(params))) } - + // Send to React Native via logEvent emission using the same working approach emitLogEvent(logData.toString()) - + // Also log to Android logs for debugging Log.d(TAG, "Analytics event: $eventName with params: $params") } catch (e: Exception) { @@ -911,9 +918,9 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) put("event", eventName) put("error_description", message) }) - + emitLogEvent(logData.toString()) - + Log.e(TAG, "Analytics error: $eventName - $message") } catch (e: Exception) { Log.e(TAG, "Error logging analytics error", e) @@ -934,10 +941,10 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) fun reset() { logAnalyticsEvent("nfc_scan_reset") apduLogger.clearContext() - + resetState() } - + /** * Generate a unique session ID for tracking passport reading sessions */ @@ -945,6 +952,41 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext) return "nfc_${System.currentTimeMillis()}_${UUID.randomUUID().toString().take(8)}" } + private fun logNfc(level: SentryLevel, message: String, stage: String, extras: Map = emptyMap()) { + val data = mutableMapOf().apply { + currentSessionId?.let { put("session_id", it) } + put("platform", "android") + put("scan_type", if (opts?.getBoolean(PARAM_USE_CAN) == true) "can" else "mrz") + put("stage", stage) + putAll(extras) + } + + if (level == SentryLevel.ERROR) { + // For errors, capture a message (this will include all previous breadcrumbs) + Sentry.withScope { scope -> + scope.level = level + currentSessionId?.let { scope.setTag("session_id", it) } + scope.setTag("platform", "android") + scope.setTag("scan_type", if (opts?.getBoolean(PARAM_USE_CAN) == true) "can" else "mrz") + scope.setTag("stage", stage) + for ((k, v) in extras) { + scope.setExtra(k, v?.toString()) + } + Sentry.captureMessage(message) + } + } else { + // For info/warn, add as breadcrumb only + Sentry.addBreadcrumb( + Breadcrumb().apply { + this.message = message + this.level = level + this.category = "nfc" + data.forEach { (key, value) -> this.data[key] = value?.toString() ?: "" } + } + ) + } + } + companion object { private val TAG = RNPassportReaderModule::class.java.simpleName private const val PARAM_DOC_NUM = "documentNumber"; diff --git a/app/ios/PassportReader.m b/app/ios/PassportReader.m index 18353cb1c..422281408 100644 --- a/app/ios/PassportReader.m +++ b/app/ios/PassportReader.m @@ -29,6 +29,7 @@ @interface RCT_EXTERN_MODULE(PassportReader, NSObject) skipCA:(NSNumber * _Nonnull)skipCA extendedMode:(NSNumber * _Nonnull)extendedMode usePacePolling:(NSNumber * _Nonnull)usePacePolling + sessionId:(NSString *)sessionId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) diff --git a/app/ios/PassportReader.swift b/app/ios/PassportReader.swift index e3b1b6c60..4a54c24d0 100644 --- a/app/ios/PassportReader.swift +++ b/app/ios/PassportReader.swift @@ -14,6 +14,7 @@ import NFCPassportReader #endif import Security import Mixpanel +import Sentry #if !E2E_TESTING @available(iOS 13, macOS 10.15, *) @@ -48,6 +49,36 @@ class PassportReader: NSObject { } private var analytics: SelfAnalytics? + private var currentSessionId: String? + + private func logNfc(level: SentryLevel, message: String, stage: String, useCANBool: Bool, sessionId: String, extras: [String: Any] = [:]) { + let data: [String: Any] = [ + "session_id": sessionId, + "platform": "ios", + "scan_type": useCANBool ? "can" : "mrz", + "stage": stage + ].merging(extras) { (_, new) in new } + + if level == .error { + // For errors, capture a message (this will include all previous breadcrumbs) + SentrySDK.configureScope { scope in + scope.setTag(value: sessionId, key: "session_id") + scope.setTag(value: "ios", key: "platform") + scope.setTag(value: useCANBool ? "can" : "mrz", key: "scan_type") + scope.setTag(value: stage, key: "stage") + for (key, value) in extras { + scope.setExtra(value: value, key: key) + } + } + SentrySDK.capture(message: message) + } else { + // For info/warn, add as breadcrumb only + let breadcrumb = Breadcrumb(level: level, category: "nfc") + breadcrumb.message = message + breadcrumb.data = data.mapValues { "\($0)" } + SentrySDK.addBreadcrumb(breadcrumb) + } + } @objc(configure:enableDebugLogs:) func configure(token: String, enableDebugLogs: Bool) { @@ -110,7 +141,7 @@ class PassportReader: NSObject { return (sum % 10) } - @objc(scanPassport:dateOfBirth:dateOfExpiry:canNumber:useCan:skipPACE:skipCA:extendedMode:usePacePolling:resolve:reject:) + @objc(scanPassport:dateOfBirth:dateOfExpiry:canNumber:useCan:skipPACE:skipCA:extendedMode:usePacePolling:sessionId:resolve:reject:) func scanPassport( _ passportNumber: String, dateOfBirth: String, @@ -121,12 +152,15 @@ class PassportReader: NSObject { skipCA: NSNumber, extendedMode: NSNumber, usePacePolling: NSNumber, + sessionId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { let useCANBool = useCan.boolValue let skipPACEBool = skipPACE.boolValue let skipCABool = skipCA.boolValue let extendedModeBool = extendedMode.boolValue let usePacePollingBool = usePacePolling.boolValue + self.currentSessionId = sessionId + logNfc(level: .info, message: "scan_start", stage: "start", useCANBool: useCANBool, sessionId: sessionId) let customMessageHandler : (NFCViewDisplayMessage)->String? = { (displayMessage) in switch displayMessage { @@ -316,9 +350,10 @@ class PassportReader: NSObject { } let stringified = String(data: try JSONEncoder().encode(ret), encoding: .utf8) - + logNfc(level: .info, message: "scan_success", stage: "complete", useCANBool: useCANBool, sessionId: sessionId) resolve(stringified) } catch { + logNfc(level: .warning, message: "scan_failed", stage: "error", useCANBool: useCANBool, sessionId: sessionId, extras: ["error": error.localizedDescription]) reject("E_PASSPORT_READ", error.localizedDescription, error) } } @@ -462,7 +497,7 @@ class PassportReader: NSObject { // No-op for E2E testing } - @objc(scanPassport:dateOfBirth:dateOfExpiry:canNumber:useCan:skipPACE:skipCA:extendedMode:usePacePolling:resolve:reject:) + @objc(scanPassport:dateOfBirth:dateOfExpiry:canNumber:useCan:skipPACE:skipCA:extendedMode:usePacePolling:sessionId:resolve:reject:) func scanPassport( _ passportNumber: String, dateOfBirth: String, @@ -473,6 +508,7 @@ class PassportReader: NSObject { skipCA: NSNumber, extendedMode: NSNumber, usePacePolling: NSNumber, + sessionId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { reject("E2E_TESTING", "NFC scanning not available in E2E testing mode", nil) } diff --git a/app/ios/Self.xcodeproj/project.pbxproj b/app/ios/Self.xcodeproj/project.pbxproj index 238b7f500..1c1507a2a 100644 --- a/app/ios/Self.xcodeproj/project.pbxproj +++ b/app/ios/Self.xcodeproj/project.pbxproj @@ -299,14 +299,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Self/Pods-Self-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Self/Pods-Self-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Self/Pods-Self-resources.sh\"\n"; @@ -343,8 +339,6 @@ "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", ); name = "[CP-User] [RNFB] Core Configuration"; - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; @@ -357,14 +351,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Self/Pods-Self-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Self/Pods-Self-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Self/Pods-Self-frameworks.sh\"\n"; @@ -795,7 +785,10 @@ "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", ); - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -885,7 +878,10 @@ "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", ); - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/app/src/Sentry.ts b/app/src/Sentry.ts index f2b7eb870..17bff1dd7 100644 --- a/app/src/Sentry.ts +++ b/app/src/Sentry.ts @@ -4,15 +4,100 @@ import { SENTRY_DSN } from '@env'; import { + addBreadcrumb, captureException as sentryCaptureException, captureFeedback as sentryCaptureFeedback, captureMessage as sentryCaptureMessage, consoleLoggingIntegration, feedbackIntegration, init as sentryInit, + withScope, wrap, } from '@sentry/react-native'; +interface BaseContext { + sessionId: string; + userId?: string; + platform: 'ios' | 'android'; + stage: string; +} + +// Security: Whitelist of allowed tag keys to prevent XSS +const ALLOWED_TAG_KEYS = new Set([ + 'session_id', + 'platform', + 'stage', + 'circuitType', + 'currentState', + 'scanType', + 'error_code', + 'proof_step', + 'scan_result', + 'verification_status', + 'document_type', +]); + +// Security: Sanitize tag values to prevent XSS +const sanitizeTagValue = (value: unknown): string => { + if (value == null) return ''; + + const stringValue = String(value); + + // Truncate to safe length + const MAX_TAG_LENGTH = 200; + const truncated = + stringValue.length > MAX_TAG_LENGTH + ? stringValue.substring(0, MAX_TAG_LENGTH) + '...' + : stringValue; + + // Escape HTML characters and remove potentially dangerous characters + return ( + truncated + .replace(/[<>&"']/g, char => { + switch (char) { + case '<': + return '<'; + case '>': + return '>'; + case '&': + return '&'; + case '"': + return '"'; + case "'": + return '''; + default: + return char; + } + }) + // Remove control characters and non-printable characters + .replace(/[^\x20-\x7E]/g, '') + ); +}; + +// Security: Sanitize tag key to prevent XSS +const sanitizeTagKey = (key: string): string | null => { + // Only allow whitelisted keys + if (!ALLOWED_TAG_KEYS.has(key)) { + return null; + } + + // Additional validation: alphanumeric and underscores only + if (!/^[a-zA-Z0-9_]+$/.test(key)) { + return null; + } + + return key; +}; + +export interface NFCScanContext extends BaseContext, Record { + scanType: 'mrz' | 'can'; +} + +export interface ProofContext extends BaseContext, Record { + circuitType: 'register' | 'dsc' | 'disclose' | null; + currentState: string; +} + export const captureException = ( error: Error, context?: Record, @@ -118,6 +203,78 @@ export const initSentry = () => { export const isSentryDisabled = !SENTRY_DSN; +type LogLevel = 'info' | 'warn' | 'error'; +type LogCategory = 'proof' | 'nfc'; + +export const logEvent = ( + level: LogLevel, + category: LogCategory, + message: string, + context: BaseContext & Record, + extra?: Record, +) => { + if (isSentryDisabled) { + return; + } + + const { sessionId, userId, platform, stage, ...rest } = context; + const data = { + session_id: sessionId, + user_id: userId, + platform, + stage, + ...rest, + ...extra, + }; + + if (level === 'error') { + withScope(scope => { + scope.setLevel('error'); + scope.setTag('session_id', sessionId); + scope.setTag('platform', platform); + scope.setTag('stage', stage); + Object.entries(rest).forEach(([key, value]) => { + const sanitizedKey = sanitizeTagKey(key); + if (sanitizedKey) { + const sanitizedValue = sanitizeTagValue(value); + scope.setTag(sanitizedKey, sanitizedValue); + } + }); + if (userId) { + scope.setUser({ id: userId }); + } + if (extra) { + Object.entries(extra).forEach(([key, value]) => { + scope.setExtra(key, value); + }); + } + sentryCaptureMessage(message); + }); + } else { + addBreadcrumb({ + message, + level: level === 'warn' ? 'warning' : 'info', + category, + data, + timestamp: Date.now() / 1000, + }); + } +}; + +export const logNFCEvent = ( + level: LogLevel, + message: string, + context: NFCScanContext, + extra?: Record, +) => logEvent(level, 'nfc', message, context, extra); + +export const logProofEvent = ( + level: LogLevel, + message: string, + context: ProofContext, + extra?: Record, +) => logEvent(level, 'proof', message, context, extra); + export const wrapWithSentry = (App: React.ComponentType) => { return isSentryDisabled ? App : wrap(App); }; diff --git a/app/src/Sentry.web.ts b/app/src/Sentry.web.ts index df8cf4210..d524f8948 100644 --- a/app/src/Sentry.web.ts +++ b/app/src/Sentry.web.ts @@ -4,14 +4,99 @@ import { SENTRY_DSN } from '@env'; import { + addBreadcrumb, captureException as sentryCaptureException, captureFeedback as sentryCaptureFeedback, captureMessage as sentryCaptureMessage, feedbackIntegration, init as sentryInit, withProfiler, + withScope, } from '@sentry/react'; +interface BaseContext { + sessionId: string; + userId?: string; + platform: 'ios' | 'android'; + stage: string; +} + +// Security: Whitelist of allowed tag keys to prevent XSS +const ALLOWED_TAG_KEYS = new Set([ + 'session_id', + 'platform', + 'stage', + 'circuitType', + 'currentState', + 'scanType', + 'error_code', + 'proof_step', + 'scan_result', + 'verification_status', + 'document_type', +]); + +// Security: Sanitize tag values to prevent XSS +const sanitizeTagValue = (value: unknown): string => { + if (value == null) return ''; + + const stringValue = String(value); + + // Truncate to safe length + const MAX_TAG_LENGTH = 200; + const truncated = + stringValue.length > MAX_TAG_LENGTH + ? stringValue.substring(0, MAX_TAG_LENGTH) + '...' + : stringValue; + + // Escape HTML characters and remove potentially dangerous characters + return ( + truncated + .replace(/[<>&"']/g, char => { + switch (char) { + case '<': + return '<'; + case '>': + return '>'; + case '&': + return '&'; + case '"': + return '"'; + case "'": + return '''; + default: + return char; + } + }) + // Remove control characters and non-printable characters + .replace(/[^\x20-\x7E]/g, '') + ); +}; + +// Security: Sanitize tag key to prevent XSS +const sanitizeTagKey = (key: string): string | null => { + // Only allow whitelisted keys + if (!ALLOWED_TAG_KEYS.has(key)) { + return null; + } + + // Additional validation: alphanumeric and underscores only + if (!/^[a-zA-Z0-9_]+$/.test(key)) { + return null; + } + + return key; +}; + +export interface NFCScanContext extends BaseContext, Record { + scanType: 'mrz' | 'can'; +} + +export interface ProofContext extends BaseContext, Record { + circuitType: 'register' | 'dsc' | 'disclose' | null; + currentState: string; +} + export const captureException = ( error: Error, context?: Record, @@ -110,6 +195,78 @@ export const initSentry = () => { export const isSentryDisabled = !SENTRY_DSN; +type LogLevel = 'info' | 'warn' | 'error'; +type LogCategory = 'proof' | 'nfc'; + +export const logEvent = ( + level: LogLevel, + category: LogCategory, + message: string, + context: BaseContext & Record, + extra?: Record, +) => { + if (isSentryDisabled) { + return; + } + + const { sessionId, userId, platform, stage, ...rest } = context; + const data = { + session_id: sessionId, + user_id: userId, + platform, + stage, + ...rest, + ...extra, + }; + + if (level === 'error') { + withScope(scope => { + scope.setLevel('error'); + scope.setTag('session_id', sessionId); + scope.setTag('platform', platform); + scope.setTag('stage', stage); + Object.entries(rest).forEach(([key, value]) => { + const sanitizedKey = sanitizeTagKey(key); + if (sanitizedKey) { + const sanitizedValue = sanitizeTagValue(value); + scope.setTag(sanitizedKey, sanitizedValue); + } + }); + if (userId) { + scope.setUser({ id: userId }); + } + if (extra) { + Object.entries(extra).forEach(([key, value]) => { + scope.setExtra(key, value); + }); + } + sentryCaptureMessage(message); + }); + } else { + addBreadcrumb({ + message, + level: level === 'warn' ? 'warning' : 'info', + category, + data, + timestamp: Date.now() / 1000, + }); + } +}; + +export const logNFCEvent = ( + level: LogLevel, + message: string, + context: NFCScanContext, + extra?: Record, +) => logEvent(level, 'nfc', message, context, extra); + +export const logProofEvent = ( + level: LogLevel, + message: string, + context: ProofContext, + extra?: Record, +) => logEvent(level, 'proof', message, context, extra); + export const wrapWithSentry = (App: React.ComponentType) => { return isSentryDisabled ? App : withProfiler(App); }; diff --git a/app/src/screens/document/DocumentNFCScanScreen.tsx b/app/src/screens/document/DocumentNFCScanScreen.tsx index 7307e6cdc..abbfe683e 100644 --- a/app/src/screens/document/DocumentNFCScanScreen.tsx +++ b/app/src/screens/document/DocumentNFCScanScreen.tsx @@ -14,6 +14,7 @@ import { import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import NfcManager from 'react-native-nfc-manager'; import { Button, Image, XStack } from 'tamagui'; +import { v4 as uuidv4 } from 'uuid'; import type { RouteProp } from '@react-navigation/native'; import { useFocusEffect, @@ -44,6 +45,7 @@ import NFC_IMAGE from '@/images/nfc.png'; import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; import { useFeedback } from '@/providers/feedbackProvider'; import { storePassportData } from '@/providers/passportDataProvider'; +import { logNFCEvent } from '@/Sentry'; import useUserStore from '@/stores/userStore'; import { flushAllAnalytics, @@ -104,6 +106,13 @@ const DocumentNFCScanScreen: React.FC = () => { const [nfcMessage, setNfcMessage] = useState(null); const scanTimeoutRef = useRef | null>(null); const scanCancelledRef = useRef(false); + const sessionIdRef = useRef(uuidv4()); + + const baseContext = { + sessionId: sessionIdRef.current, + platform: Platform.OS as 'ios' | 'android', + scanType: route.params?.useCan ? 'can' : 'mrz', + } as const; const animationRef = useRef(null); @@ -111,6 +120,16 @@ const DocumentNFCScanScreen: React.FC = () => { animationRef.current?.play(); }, []); + useEffect(() => { + logNFCEvent('info', 'screen_mount', { ...baseContext, stage: 'mount' }); + return () => { + logNFCEvent('info', 'screen_unmount', { + ...baseContext, + stage: 'unmount', + }); + }; + }, []); + // Cleanup timeout on component unmount useEffect(() => { return () => { @@ -144,6 +163,15 @@ const DocumentNFCScanScreen: React.FC = () => { const openErrorModal = useCallback( (message: string) => { flushAllAnalytics(); + logNFCEvent( + 'error', + 'nfc_error_modal', + { + ...baseContext, + stage: 'error', + }, + { message: sanitizeErrorMessage(message) }, + ); showModal({ titleText: 'NFC Scan Error', bodyText: message, @@ -171,6 +199,18 @@ const DocumentNFCScanScreen: React.FC = () => { setDialogMessage('NFC is not enabled. Please enable it in settings.'); } setIsNfcSupported(true); + logNFCEvent( + 'info', + 'nfc_capability', + { + ...baseContext, + stage: 'check', + }, + { + supported: true, + enabled: isEnabled, + }, + ); } else { setDialogMessage( "Sorry, your device doesn't seem to have an NFC reader.", @@ -179,6 +219,18 @@ const DocumentNFCScanScreen: React.FC = () => { // near the disabled button when NFC isn't supported setIsNfcEnabled(false); setIsNfcSupported(false); + logNFCEvent( + 'warn', + 'nfc_capability', + { + ...baseContext, + stage: 'check', + }, + { + supported: false, + enabled: false, + }, + ); } }, []); @@ -200,7 +252,12 @@ const DocumentNFCScanScreen: React.FC = () => { const onVerifyPress = useCallback(async () => { buttonTap(); if (isNfcEnabled) { + logNFCEvent('info', 'verify_pressed', { + ...baseContext, + stage: 'ui', + }); setIsNfcSheetOpen(true); + logNFCEvent('info', 'sheet_open', { ...baseContext, stage: 'ui' }); // Add timestamp when scan starts scanCancelledRef.current = false; const scanStartTime = Date.now(); @@ -213,8 +270,16 @@ const DocumentNFCScanScreen: React.FC = () => { trackEvent(PassportEvents.NFC_SCAN_FAILED, { error: 'timeout', }); + logNFCEvent('warn', 'scan_timeout', { + ...baseContext, + stage: 'timeout', + }); openErrorModal('Scan timed out. Please try again.'); setIsNfcSheetOpen(false); + logNFCEvent('info', 'sheet_close', { + ...baseContext, + stage: 'ui', + }); }, 30000); // Mark NFC scanning as active to prevent analytics flush interference @@ -233,8 +298,16 @@ const DocumentNFCScanScreen: React.FC = () => { trackNfcEvent(PassportEvents.NFC_SCAN_FAILED, { error: 'timeout', }); + logNFCEvent('warn', 'scan_timeout', { + ...baseContext, + stage: 'timeout', + }); openErrorModal('Scan timed out. Please try again.'); setIsNfcSheetOpen(false); + logNFCEvent('info', 'sheet_close', { + ...baseContext, + stage: 'ui', + }); }, 30000); try { @@ -251,6 +324,7 @@ const DocumentNFCScanScreen: React.FC = () => { skipCA, extendedMode, usePacePolling: isPacePolling, + sessionId: sessionIdRef.current, }); // Check if scan was cancelled by timeout @@ -270,6 +344,15 @@ const DocumentNFCScanScreen: React.FC = () => { trackEvent(PassportEvents.NFC_SCAN_SUCCESS, { duration_seconds: parseFloat(scanDurationSeconds), }); + logNFCEvent( + 'info', + 'scan_success', + { + ...baseContext, + stage: 'complete', + }, + { duration_seconds: parseFloat(scanDurationSeconds) }, + ); let passportData: PassportData | null = null; let parsedPassportData: PassportData | null = null; try { @@ -388,6 +471,7 @@ const DocumentNFCScanScreen: React.FC = () => { scanTimeoutRef.current = null; } setIsNfcSheetOpen(false); + logNFCEvent('info', 'sheet_close', { ...baseContext, stage: 'ui' }); setNfcScanningActive(false); } } else if (isNfcSupported) { @@ -419,6 +503,7 @@ const DocumentNFCScanScreen: React.FC = () => { const onCancelPress = async () => { flushAllAnalytics(); + logNFCEvent('info', 'scan_cancelled', { ...baseContext, stage: 'cancel' }); const hasValidDocument = await hasAnyValidRegisteredDocument(selfClient); if (hasValidDocument) { navigateToHome(); @@ -435,6 +520,7 @@ const DocumentNFCScanScreen: React.FC = () => { useFocusEffect( useCallback(() => { + logNFCEvent('info', 'screen_focus', { ...baseContext, stage: 'focus' }); checkNfcSupport(); if (Platform.OS === 'android' && emitter) { @@ -469,6 +555,7 @@ const DocumentNFCScanScreen: React.FC = () => { ); return () => { + logNFCEvent('info', 'screen_blur', { ...baseContext, stage: 'blur' }); subscription.remove(); // Clear scan timeout when component loses focus scanCancelledRef.current = true; @@ -481,6 +568,7 @@ const DocumentNFCScanScreen: React.FC = () => { // For iOS or when no emitter, still handle timeout cleanup on blur return () => { + logNFCEvent('info', 'screen_blur', { ...baseContext, stage: 'blur' }); scanCancelledRef.current = true; if (scanTimeoutRef.current) { clearTimeout(scanTimeoutRef.current); diff --git a/app/src/types/react-native-passport-reader.d.ts b/app/src/types/react-native-passport-reader.d.ts index 3a347402d..df5d16167 100644 --- a/app/src/types/react-native-passport-reader.d.ts +++ b/app/src/types/react-native-passport-reader.d.ts @@ -10,6 +10,7 @@ declare module 'react-native-passport-reader' { canNumber: string; useCan: boolean; quality?: number; + sessionId?: string; } interface PassportReader { @@ -27,6 +28,7 @@ declare module 'react-native-passport-reader' { skipCA: boolean, extendedMode: boolean, usePacePolling: boolean, + sessionId: string, ): Promise<{ mrz: string; eContent: string; diff --git a/app/src/utils/nfcScanner.ts b/app/src/utils/nfcScanner.ts index 487825ca8..1094299d2 100644 --- a/app/src/utils/nfcScanner.ts +++ b/app/src/utils/nfcScanner.ts @@ -7,6 +7,7 @@ import { Platform } from 'react-native'; import type { PassportData } from '@selfxyz/common/types'; +import { logNFCEvent, type NFCScanContext } from '@/Sentry'; import { configureNfcAnalytics } from '@/utils/analytics'; import { PassportReader, @@ -39,6 +40,8 @@ interface Inputs { skipCA?: boolean; extendedMode?: boolean; usePacePolling?: boolean; + sessionId: string; + userId?: string; } export const parseScanResponse = (response: unknown) => { @@ -50,18 +53,46 @@ export const parseScanResponse = (response: unknown) => { export const scan = async (inputs: Inputs) => { await configureNfcAnalytics(); - return Platform.OS === 'android' - ? await scanAndroid(inputs) - : await scanIOS(inputs); + const baseContext = { + sessionId: inputs.sessionId, + userId: inputs.userId, + platform: Platform.OS as 'ios' | 'android', + scanType: inputs.useCan ? 'can' : 'mrz', + } as const; + + logNFCEvent('info', 'scan_start', { ...baseContext, stage: 'start' }); + + try { + return Platform.OS === 'android' + ? await scanAndroid(inputs, baseContext) + : await scanIOS(inputs, baseContext); + } catch (error) { + logNFCEvent( + 'error', + 'scan_failed', + { ...baseContext, stage: 'scan' }, + { + error: error instanceof Error ? error.message : String(error), + }, + ); + throw error; + } }; -const scanAndroid = async (inputs: Inputs) => { +const scanAndroid = async ( + inputs: Inputs, + context: Omit, +) => { reset(); if (!scanDocument) { console.warn( 'Android passport scanner is not available - native module failed to load', ); + logNFCEvent('error', 'module_unavailable', { + ...context, + stage: 'init', + } as NFCScanContext); return Promise.reject(new Error('NFC scanning is currently unavailable.')); } @@ -71,14 +102,22 @@ const scanAndroid = async (inputs: Inputs) => { dateOfExpiry: inputs.dateOfExpiry, canNumber: inputs.canNumber ?? '', useCan: inputs.useCan ?? false, + sessionId: inputs.sessionId, }); }; -const scanIOS = async (inputs: Inputs) => { +const scanIOS = async ( + inputs: Inputs, + context: Omit, +) => { if (!PassportReader?.scanPassport) { console.warn( 'iOS passport scanner is not available - native module failed to load', ); + logNFCEvent('error', 'module_unavailable', { + ...context, + stage: 'init', + } as NFCScanContext); return Promise.reject( new Error( 'NFC scanning is currently unavailable. Please ensure the app is properly installed.', @@ -97,6 +136,7 @@ const scanIOS = async (inputs: Inputs) => { inputs.skipCA ?? false, inputs.extendedMode ?? false, inputs.usePacePolling ?? false, + inputs.sessionId, ), ); }; diff --git a/app/src/utils/passportReader.ts b/app/src/utils/passportReader.ts index bb8431784..41ddbbef9 100644 --- a/app/src/utils/passportReader.ts +++ b/app/src/utils/passportReader.ts @@ -14,6 +14,7 @@ type ScanOptions = { skipCA?: boolean; extendedMode?: boolean; usePacePolling?: boolean; + sessionId?: string; }; // Platform-specific PassportReader implementation @@ -55,6 +56,7 @@ if (Platform.OS === 'android') { skipCA = false, extendedMode = false, usePacePolling = true, + sessionId = '', } = options; const result = await PassportReader.scanPassport( @@ -67,6 +69,7 @@ if (Platform.OS === 'android') { skipCA, extendedMode, usePacePolling, + sessionId, ); // iOS native returns a JSON string; normalize to object. try { diff --git a/app/src/utils/proving/provingMachine.ts b/app/src/utils/proving/provingMachine.ts index 6d1c41102..d8280add1 100644 --- a/app/src/utils/proving/provingMachine.ts +++ b/app/src/utils/proving/provingMachine.ts @@ -3,10 +3,11 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. import forge from 'node-forge'; +import { Platform } from 'react-native'; import type { Socket } from 'socket.io-client'; import socketIo from 'socket.io-client'; import { v4 } from 'uuid'; -import type { AnyActorRef, StateFrom } from 'xstate'; +import type { AnyActorRef, AnyEventObject, StateFrom } from 'xstate'; import { createActor, createMachine } from 'xstate'; import { create } from 'zustand'; @@ -37,6 +38,7 @@ import { getPayload, getWSDbRelayerUrl, } from '@selfxyz/common/utils/proving'; +import type { SelfClient } from '@selfxyz/mobile-sdk-alpha'; import { clearPassportData, hasAnyValidRegisteredDocument, @@ -44,7 +46,6 @@ import { markCurrentDocumentAsRegistered, reStorePassportDataWithRightCSCA, SdkEvents, - SelfClient, } from '@selfxyz/mobile-sdk-alpha'; import { PassportEvents, @@ -55,6 +56,159 @@ import { useSelfAppStore, } from '@selfxyz/mobile-sdk-alpha/stores'; +import { logProofEvent, type ProofContext } from '@/Sentry'; +// import analytics from '@/utils/analytics'; +import { + handleStatusCode, + parseStatusMessage, +} from '@/utils/proving/statusHandlers'; + +// Helper functions for WebSocket URL resolution +const getMappingKey = ( + circuitType: 'disclose' | 'register' | 'dsc', + documentCategory: DocumentCategory, +): string => { + if (circuitType === 'disclose') { + return documentCategory === 'passport' ? 'DISCLOSE' : 'DISCLOSE_ID'; + } + if (circuitType === 'register') { + return documentCategory === 'passport' ? 'REGISTER' : 'REGISTER_ID'; + } + // circuitType === 'dsc' + return documentCategory === 'passport' ? 'DSC' : 'DSC_ID'; +}; + +const resolveWebSocketUrl = ( + circuitType: 'disclose' | 'register' | 'dsc', + passportData: PassportData, + circuitName: string, +): string | undefined => { + const { documentCategory } = passportData; + const circuitsMapping = + useProtocolStore.getState()[documentCategory].circuits_dns_mapping; + const mappingKey = getMappingKey(circuitType, documentCategory); + + return circuitsMapping?.[mappingKey]?.[circuitName]; +}; + +// Helper functions for _generatePayload refactoring +const _generateCircuitInputs = ( + circuitType: 'disclose' | 'register' | 'dsc', + secret: string | undefined | null, + passportData: PassportData, + env: 'prod' | 'stg', +) => { + const document: DocumentCategory = passportData.documentCategory; + const protocolStore = useProtocolStore.getState(); + const selfApp = useSelfAppStore.getState().selfApp; + + let inputs, + circuitName, + endpointType, + endpoint, + circuitTypeWithDocumentExtension; + + switch (circuitType) { + case 'register': + ({ inputs, circuitName, endpointType, endpoint } = + generateTEEInputsRegister( + secret as string, + passportData, + protocolStore[document].dsc_tree, + env, + )); + circuitTypeWithDocumentExtension = `${circuitType}${document === 'passport' ? '' : '_id'}`; + break; + case 'dsc': + ({ inputs, circuitName, endpointType, endpoint } = generateTEEInputsDSC( + passportData, + protocolStore[document].csca_tree as string[][], + env, + )); + circuitTypeWithDocumentExtension = `${circuitType}${document === 'passport' ? '' : '_id'}`; + break; + case 'disclose': + ({ inputs, circuitName, endpointType, endpoint } = + generateTEEInputsDiscloseStateless( + secret as string, + passportData, + selfApp as SelfApp, + (doc: DocumentCategory, tree) => { + const docStore = + doc === 'passport' + ? protocolStore.passport + : protocolStore.id_card; + switch (tree) { + case 'ofac': + return docStore.ofac_trees; + case 'commitment': + if (!docStore.commitment_tree) { + throw new Error('Commitment tree not loaded'); + } + return docStore.commitment_tree; + default: + throw new Error('Unknown tree type'); + } + }, + )); + circuitTypeWithDocumentExtension = `disclose`; + break; + default: + throw new Error('Invalid circuit type:' + circuitType); + } + + return { + inputs, + circuitName, + endpointType, + endpoint, + circuitTypeWithDocumentExtension, + }; +}; + +const JSONRPC_VERSION = '2.0' as const; +const SUBMIT_METHOD = 'openpassport_submit_request' as const; +const SUBMIT_ID = 2 as const; + +type EncryptedPayload = { + nonce: number[]; + cipher_text: number[]; + auth_tag: number[]; +}; + +type SubmitRequest = { + jsonrpc: typeof JSONRPC_VERSION; + method: typeof SUBMIT_METHOD; + id: typeof SUBMIT_ID; + params: { uuid: string | null } & EncryptedPayload; +}; + +const _encryptPayload = ( + payload: unknown, + sharedKey: Buffer, +): EncryptedPayload => { + const forgeKey = forge.util.createBuffer(sharedKey.toString('binary')); + return encryptAES256GCM(JSON.stringify(payload), forgeKey); +}; + +const _buildSubmitRequest = ( + uuid: string | null, + encryptedPayload: EncryptedPayload, +): SubmitRequest => { + return { + jsonrpc: JSONRPC_VERSION, + method: SUBMIT_METHOD, + id: SUBMIT_ID, + params: { + uuid: uuid, + ...encryptedPayload, + }, + }; +}; + +const getPlatform = (): 'ios' | 'android' => + Platform.OS === 'ios' ? 'ios' : 'android'; + export type ProvingStateType = // Initial states | 'idle' @@ -237,8 +391,21 @@ export const useProvingStore = create((set, get) => { newActor: AnyActorRef, selfClient: SelfClient, ) { + let lastTransition = Date.now(); + let lastEvent: AnyEventObject = { type: 'init' }; + newActor.on('*', (event: AnyEventObject) => { + lastEvent = event; + }); newActor.subscribe((state: StateFrom) => { - console.log(`State transition: ${state.value}`); + const now = Date.now(); + const context = createProofContext('stateTransition', { + currentState: String(state.value), + }); + logProofEvent('info', `State transition: ${state.value}`, context, { + event: lastEvent.type, + duration_ms: now - lastTransition, + }); + lastTransition = now; selfClient.trackEvent(ProofEvents.PROVING_STATE_CHANGE, { state: state.value, }); @@ -359,10 +526,17 @@ export const useProvingStore = create((set, get) => { console.error('Cannot process message: State machine not initialized.'); return; } + + const startTime = Date.now(); + const context = createProofContext('_handleWebSocketMessage'); + try { const result = JSON.parse(event.data); + logProofEvent('info', 'WebSocket message received', context); if (result.result?.attestation) { selfClient?.trackEvent(ProofEvents.ATTESTATION_RECEIVED); + logProofEvent('info', 'Attestation received', context); + const attestationData = result.result.attestation; set({ attestation: attestationData }); @@ -370,12 +544,17 @@ export const useProvingStore = create((set, get) => { const verified = await verifyAttestation(attestationData); if (!verified) { + logProofEvent('error', 'Attestation verification failed', context, { + failure: 'PROOF_FAILED_TEE_PROCESSING', + duration_ms: Date.now() - startTime, + }); console.error('Attestation verification failed'); actor!.send({ type: 'CONNECT_ERROR' }); return; } selfClient?.trackEvent(ProofEvents.ATTESTATION_VERIFIED); + logProofEvent('info', 'Attestation verified', context); const serverKey = ec.keyFromPublic(serverPubkey as string, 'hex'); const derivedKey = clientKey.derive(serverKey.getPublic()); @@ -385,6 +564,7 @@ export const useProvingStore = create((set, get) => { sharedKey: Buffer.from(derivedKey.toArray('be', 32)), }); selfClient?.trackEvent(ProofEvents.SHARED_KEY_DERIVED); + logProofEvent('info', 'Shared key derived', context); actor!.send({ type: 'CONNECT_SUCCESS' }); } else if ( @@ -393,17 +573,24 @@ export const useProvingStore = create((set, get) => { !result.error ) { selfClient?.trackEvent(ProofEvents.WS_HELLO_ACK); + logProofEvent('info', 'Hello ACK received', context); + // Received status from TEE const statusUuid = result.result; if (get().uuid !== statusUuid) { + logProofEvent('warn', 'Status UUID mismatch', context, { + received_uuid: statusUuid, + }); console.warn( - `Received status UUID (${statusUuid}) does not match stored UUID (${ - get().uuid - }). Using received UUID.`, + `Received status UUID (${statusUuid}) does not match stored UUID (${get().uuid}). Using received UUID.`, ); } const endpointType = get().endpointType; if (!endpointType) { + logProofEvent('error', 'Endpoint type missing', context, { + failure: 'PROOF_FAILED_TEE_PROCESSING', + duration_ms: Date.now() - startTime, + }); console.error( 'Cannot start Socket.IO listener: endpointType not set.', ); @@ -420,6 +607,11 @@ export const useProvingStore = create((set, get) => { selfClient, ); } else if (result.error) { + logProofEvent('error', 'TEE returned error', context, { + failure: 'PROOF_FAILED_TEE_PROCESSING', + error: result.error, + duration_ms: Date.now() - startTime, + }); console.error('Received error from TEE:', result.error); selfClient?.trackEvent(ProofEvents.TEE_WS_ERROR, { error: result.error, @@ -430,9 +622,18 @@ export const useProvingStore = create((set, get) => { }); actor!.send({ type: 'PROVE_ERROR' }); } else { + logProofEvent('warn', 'Unknown message format', context); console.warn('Received unknown message format from TEE:', result); } } catch (error) { + logProofEvent('error', 'WebSocket message handling failed', context, { + failure: + get().currentState === 'init_tee_connexion' + ? 'PROOF_FAILED_CONNECTION' + : 'PROOF_FAILED_TEE_PROCESSING', + error: error instanceof Error ? error.message : String(error), + duration_ms: Date.now() - startTime, + }); console.error('Error processing WebSocket message:', error); if (get().currentState === 'init_tee_connexion') { selfClient?.trackEvent(ProofEvents.TEE_CONN_FAILED, { @@ -451,7 +652,6 @@ export const useProvingStore = create((set, get) => { } } }, - _handleRegisterErrorOrFailure: async (selfClient: SelfClient) => { try { const hasValid = await hasAnyValidRegisteredDocument(selfClient); @@ -475,7 +675,6 @@ export const useProvingStore = create((set, get) => { console.error('Cannot start Socket.IO listener: Actor not available.'); return; } - const url = getWSDbRelayerUrl(endpointType); const socket: Socket = socketIo(url, { path: '/', @@ -483,10 +682,13 @@ export const useProvingStore = create((set, get) => { }); set({ socketConnection: socket }); selfClient.trackEvent(ProofEvents.SOCKETIO_CONN_STARTED); + const context = createProofContext('_startSocketIOStatusListener'); + logProofEvent('info', 'Socket.IO listener started', context, { url }); socket.on('connect', () => { socket?.emit('subscribe', receivedUuid); selfClient.trackEvent(ProofEvents.SOCKETIO_SUBSCRIBED); + logProofEvent('info', 'Socket.IO connected', context); }); socket.on('connect_error', error => { @@ -494,9 +696,9 @@ export const useProvingStore = create((set, get) => { selfClient.trackEvent(ProofEvents.SOCKETIO_CONNECT_ERROR, { message: error instanceof Error ? error.message : String(error), }); - selfClient.trackEvent(ProofEvents.PROOF_FAILED, { - circuitType: get().circuitType, - error: get().error_code ?? 'unknown', + logProofEvent('error', 'Socket.IO connection error', context, { + failure: 'PROOF_FAILED_CONNECTION', + error: error instanceof Error ? error.message : String(error), }); actor!.send({ type: 'PROVE_ERROR' }); set({ socketConnection: null }); @@ -504,52 +706,82 @@ export const useProvingStore = create((set, get) => { socket.on('disconnect', (_reason: string) => { const currentActor = actor; - + logProofEvent('warn', 'Socket.IO disconnected', context); if (get().currentState === 'ready_to_prove' && currentActor) { console.error( 'SocketIO disconnected unexpectedly during proof listening.', ); selfClient.trackEvent(ProofEvents.SOCKETIO_DISCONNECT_UNEXPECTED); - selfClient.trackEvent(ProofEvents.PROOF_FAILED, { - circuitType: get().circuitType, - error: get().error_code ?? 'unknown', - }); + logProofEvent( + 'error', + 'Socket.IO disconnected unexpectedly', + context, + { + failure: 'PROOF_FAILED_CONNECTION', + }, + ); currentActor.send({ type: 'PROVE_ERROR' }); } set({ socketConnection: null }); }); socket.on('status', (message: unknown) => { - const data = - typeof message === 'string' ? JSON.parse(message) : message; - selfClient.trackEvent(ProofEvents.SOCKETIO_STATUS_RECEIVED, { - status: data.status, - }); - if (data.status === 3 || data.status === 5) { - console.error( - 'Proof generation/verification failed (status 3 or 5).', - ); - console.error(data); - set({ error_code: data.error_code, reason: data.reason }); - selfClient.trackEvent(ProofEvents.SOCKETIO_PROOF_FAILURE, { - error_code: data.error_code, - reason: data.reason, + try { + const data = parseStatusMessage(message); + + selfClient.trackEvent(ProofEvents.SOCKETIO_STATUS_RECEIVED, { + status: data.status, }); - selfClient.trackEvent(ProofEvents.PROOF_FAILED, { - circuitType: get().circuitType, - error: data.error_code ?? 'unknown', + logProofEvent('info', 'Status message received', context, { + status: data.status, }); - actor!.send({ type: 'PROVE_FAILURE' }); - socket?.disconnect(); - set({ socketConnection: null }); - } else if (data.status === 4) { - socket?.disconnect(); - set({ socketConnection: null }); - if (get().circuitType === 'register') { - selfClient.trackEvent(ProofEvents.REGISTER_COMPLETED); + + const result = handleStatusCode(data, get().circuitType as string); + + // Handle state updates + if (result.stateUpdate) { + set(result.stateUpdate); } - selfClient.trackEvent(ProofEvents.SOCKETIO_PROOF_SUCCESS); - actor!.send({ type: 'PROVE_SUCCESS' }); + + // Handle analytics + result.analytics?.forEach(({ event, data: eventData }) => { + if (event === 'SOCKETIO_PROOF_FAILURE') { + logProofEvent('error', 'TEE processing failed', context, { + failure: 'PROOF_FAILED_TEE_PROCESSING', + error_code: eventData?.error_code, + reason: eventData?.reason, + }); + } else if (event === 'SOCKETIO_PROOF_SUCCESS') { + logProofEvent('info', 'TEE processing succeeded', context); + } + selfClient.trackEvent( + event as unknown as keyof typeof ProofEvents, + eventData, + ); + }); + + // Handle actor events + if (result.actorEvent) { + if (result.actorEvent.type === 'PROVE_FAILURE') { + console.error( + 'Proof generation/verification failed (status 3 or 5).', + ); + console.error(data); + } + actor!.send(result.actorEvent); + } + + // Handle disconnection + if (result.shouldDisconnect) { + socket?.disconnect(); + } + } catch (error) { + console.error('Error handling status message:', error); + logProofEvent('error', 'Status message parsing failed', context, { + failure: 'PROOF_FAILED_MESSAGE_PARSING', + error: error instanceof Error ? error.message : String(error), + }); + actor!.send({ type: 'PROVE_ERROR' }); } }); }, @@ -567,7 +799,10 @@ export const useProvingStore = create((set, get) => { selfClient.trackEvent(ProofEvents.CONNECTION_UUID_GENERATED, { connection_uuid: connectionUuid, }); - + const context = createProofContext('_handleWsOpen', { + sessionId: connectionUuid, + }); + logProofEvent('info', 'WebSocket open', context); set({ uuid: connectionUuid }); const helloBody = { jsonrpc: '2.0', @@ -583,6 +818,7 @@ export const useProvingStore = create((set, get) => { }; selfClient.trackEvent(ProofEvents.WS_HELLO_SENT); ws.send(JSON.stringify(helloBody)); + logProofEvent('info', 'WS hello sent', context); }, _handleWsError: (error: Event, selfClient: SelfClient) => { @@ -590,6 +826,11 @@ export const useProvingStore = create((set, get) => { if (!actor) { return; } + const context = createProofContext('_handleWsError'); + logProofEvent('error', 'TEE WebSocket error', context, { + failure: 'PROOF_FAILED_CONNECTION', + error: error instanceof Error ? error.message : String(error), + }); get()._handleWebSocketMessage( new MessageEvent('error', { data: JSON.stringify({ error: 'WebSocket connection error' }), @@ -606,6 +847,11 @@ export const useProvingStore = create((set, get) => { if (!actor) { return; } + const context = createProofContext('_handleWsClose'); + logProofEvent('warn', 'TEE WebSocket closed', context, { + code: event.code, + reason: event.reason, + }); const currentState = get().currentState; if ( currentState === 'init_tee_connexion' || @@ -695,12 +941,19 @@ export const useProvingStore = create((set, get) => { startFetchingData: async (selfClient: SelfClient) => { _checkActorInitialized(actor); selfClient.trackEvent(ProofEvents.FETCH_DATA_STARTED); + const startTime = Date.now(); + const context = createProofContext('startFetchingData'); + logProofEvent('info', 'Fetching DSC data started', context); try { const { passportData, env } = get(); if (!passportData) { throw new Error('PassportData is not available'); } if (!passportData?.dsc_parsed) { + logProofEvent('error', 'Missing parsed DSC', context, { + failure: 'PROOF_FAILED_DATA_FETCH', + duration_ms: Date.now() - startTime, + }); console.error('Missing parsed DSC in passport data'); selfClient.trackEvent(ProofEvents.FETCH_DATA_FAILED, { message: 'Missing parsed DSC in passport data', @@ -709,14 +962,26 @@ export const useProvingStore = create((set, get) => { return; } const document: DocumentCategory = passportData.documentCategory; + logProofEvent('info', 'Protocol store fetch', context, { + step: 'protocol_store_fetch', + document, + }); await useProtocolStore .getState() [ document ].fetch_all(env!, (passportData as PassportData).dsc_parsed!.authorityKeyIdentifier); + logProofEvent('info', 'Data fetch succeeded', context, { + duration_ms: Date.now() - startTime, + }); selfClient.trackEvent(ProofEvents.FETCH_DATA_SUCCESS); actor!.send({ type: 'FETCH_SUCCESS' }); } catch (error) { + logProofEvent('error', 'Data fetch failed', context, { + failure: 'PROOF_FAILED_DATA_FETCH', + error: error instanceof Error ? error.message : String(error), + duration_ms: Date.now() - startTime, + }); console.error('Error fetching data:', error); selfClient.trackEvent(ProofEvents.FETCH_DATA_FAILED, { message: error instanceof Error ? error.message : String(error), @@ -729,6 +994,9 @@ export const useProvingStore = create((set, get) => { _checkActorInitialized(actor); // TODO: for the disclosure, we could check that the selfApp is a valid one. selfClient.trackEvent(ProofEvents.VALIDATION_STARTED); + const startTime = Date.now(); + const context = createProofContext('validatingDocument'); + logProofEvent('info', 'Validating document started', context); try { const { passportData, secret, circuitType } = get(); if (!passportData) { @@ -738,7 +1006,16 @@ export const useProvingStore = create((set, get) => { getDeployedCircuits: (documentCategory: DocumentCategory) => useProtocolStore.getState()[documentCategory].deployed_circuits!, }); + logProofEvent('info', 'Document support check', context, { + supported: isSupported.status === 'passport_supported', + duration_ms: Date.now() - startTime, + }); if (isSupported.status !== 'passport_supported') { + logProofEvent('error', 'Passport not supported', context, { + failure: 'PROOF_FAILED_VALIDATION', + details: isSupported.details, + duration_ms: Date.now() - startTime, + }); console.error( 'Passport not supported:', isSupported.status, @@ -758,17 +1035,26 @@ export const useProvingStore = create((set, get) => { useProtocolStore.getState()[documentCategory].commitment_tree; /// disclosure if (circuitType === 'disclose') { - // check if the user is registered using the csca from the passport data. const isRegisteredWithLocalCSCA = await isUserRegistered( passportData, secret as string, getCommitmentTree, ); + logProofEvent('info', 'Local CSCA registration check', context, { + registered: isRegisteredWithLocalCSCA, + }); if (isRegisteredWithLocalCSCA) { + logProofEvent('info', 'Validation succeeded', context, { + duration_ms: Date.now() - startTime, + }); selfClient.trackEvent(ProofEvents.VALIDATION_SUCCESS); actor!.send({ type: 'VALIDATION_SUCCESS' }); return; } else { + logProofEvent('error', 'Passport data not found', context, { + failure: 'PROOF_FAILED_VALIDATION', + duration_ms: Date.now() - startTime, + }); actor!.send({ type: 'PASSPORT_DATA_NOT_FOUND' }); return; } @@ -786,6 +1072,14 @@ export const useProvingStore = create((set, get) => { useProtocolStore.getState()[docType].alternative_csca, }, ); + logProofEvent( + 'info', + 'Alternative CSCA registration check', + context, + { + registered: isRegistered, + }, + ); if (isRegistered) { await reStorePassportDataWithRightCSCA( selfClient, @@ -793,22 +1087,30 @@ export const useProvingStore = create((set, get) => { csca as string, ); - // Mark document as registered since its already onChain (async () => { try { await markCurrentDocumentAsRegistered(selfClient); } catch (error) { - //it will be checked and marked as registered during next app launch console.error('Error marking document as registered:', error); } })(); selfClient.trackEvent(ProofEvents.ALREADY_REGISTERED); + logProofEvent('info', 'Document already registered', context, { + duration_ms: Date.now() - startTime, + }); actor!.send({ type: 'ALREADY_REGISTERED' }); return; } const isNullifierOnchain = await isDocumentNullified(passportData); + logProofEvent('info', 'Nullifier check', context, { + nullified: isNullifierOnchain, + }); if (isNullifierOnchain) { + logProofEvent('error', 'Passport nullified', context, { + failure: 'PROOF_FAILED_VALIDATION', + duration_ms: Date.now() - startTime, + }); console.warn( 'Passport is nullified, but not registered with this secret. Navigating to AccountRecoveryChoice', ); @@ -821,14 +1123,25 @@ export const useProvingStore = create((set, get) => { passportData, useProtocolStore.getState()[document].dsc_tree, ); + logProofEvent('info', 'DSC tree check', context, { + dsc_registered: isDscRegistered, + }); if (isDscRegistered) { selfClient.trackEvent(ProofEvents.DSC_IN_TREE); set({ circuitType: 'register' }); } + logProofEvent('info', 'Validation succeeded', context, { + duration_ms: Date.now() - startTime, + }); selfClient.trackEvent(ProofEvents.VALIDATION_SUCCESS); actor!.send({ type: 'VALIDATION_SUCCESS' }); } } catch (error) { + logProofEvent('error', 'Validation failed', context, { + failure: 'PROOF_FAILED_VALIDATION', + error: error instanceof Error ? error.message : String(error), + duration_ms: Date.now() - startTime, + }); console.error('Error validating passport:', error); selfClient.trackEvent(ProofEvents.VALIDATION_FAILED, { message: error instanceof Error ? error.message : String(error), @@ -838,62 +1151,77 @@ export const useProvingStore = create((set, get) => { }, initTeeConnection: async (selfClient: SelfClient): Promise => { + const startTime = Date.now(); + const baseContext = createProofContext('initTeeConnection'); const { passportData } = get(); if (!passportData) { + logProofEvent('error', 'Passport data missing', baseContext, { + failure: 'PROOF_FAILED_CONNECTION', + duration_ms: Date.now() - startTime, + }); throw new Error('PassportData is not available'); } - const document: DocumentCategory = passportData.documentCategory; - const circuitsMapping = - useProtocolStore.getState()[document].circuits_dns_mapping; + const document: DocumentCategory = (passportData as PassportData) + .documentCategory; + const circuitType = get().circuitType as 'disclose' | 'register' | 'dsc'; - let circuitName, wsRpcUrl; - if (get().circuitType === 'disclose') { + let circuitName; + if (circuitType === 'disclose') { circuitName = 'disclose'; - if (passportData.documentCategory === 'passport') { - wsRpcUrl = circuitsMapping?.DISCLOSE?.[circuitName]; - } else { - wsRpcUrl = circuitsMapping?.DISCLOSE_ID?.[circuitName]; - } } else { circuitName = getCircuitNameFromPassportData( passportData, - get().circuitType as 'register' | 'dsc', + circuitType as 'register' | 'dsc', ); - if (get().circuitType === 'register') { - if (passportData.documentCategory === 'passport') { - wsRpcUrl = circuitsMapping?.REGISTER?.[circuitName]; - } else { - wsRpcUrl = circuitsMapping?.REGISTER_ID?.[circuitName]; - } - } else { - if (passportData.documentCategory === 'passport') { - wsRpcUrl = circuitsMapping?.DSC?.[circuitName]; - } else { - wsRpcUrl = circuitsMapping?.DSC_ID?.[circuitName]; - } - } } + + const wsRpcUrl = resolveWebSocketUrl( + circuitType, + passportData as PassportData, + circuitName, + ); + logProofEvent('info', 'Circuit resolution', baseContext, { + circuit_name: circuitName, + ws_url: wsRpcUrl, + }); if (!circuitName) { actor?.send({ type: 'CONNECT_ERROR' }); + logProofEvent('error', 'Circuit name missing', baseContext, { + failure: 'PROOF_FAILED_CONNECTION', + duration_ms: Date.now() - startTime, + }); throw new Error('Could not determine circuit name'); } if (!wsRpcUrl) { actor?.send({ type: 'CONNECT_ERROR' }); + logProofEvent('error', 'WebSocket URL missing', baseContext, { + failure: 'PROOF_FAILED_CONNECTION', + duration_ms: Date.now() - startTime, + }); throw new Error('No WebSocket URL available for TEE connection'); } get()._closeConnections(selfClient); selfClient.trackEvent(ProofEvents.TEE_CONN_STARTED); + logProofEvent('info', 'TEE connection attempt', baseContext); return new Promise(resolve => { const ws = new WebSocket(wsRpcUrl); const handleConnectSuccess = () => { + logProofEvent('info', 'TEE connection succeeded', baseContext, { + duration_ms: Date.now() - startTime, + }); selfClient.trackEvent(ProofEvents.TEE_CONN_SUCCESS); resolve(true); }; const handleConnectError = (msg: string = 'connect_error') => { + logProofEvent('error', 'TEE connection failed', baseContext, { + failure: 'PROOF_FAILED_CONNECTION', + error: msg, + duration_ms: Date.now() - startTime, + }); selfClient.trackEvent(ProofEvents.TEE_CONN_FAILED, { message: msg }); resolve(false); }; @@ -931,27 +1259,32 @@ export const useProvingStore = create((set, get) => { startProving: async (selfClient: SelfClient) => { _checkActorInitialized(actor); + const startTime = Date.now(); const { wsConnection, sharedKey, passportData, secret, uuid, fcmToken } = get(); + const context = createProofContext('startProving', { + sessionId: uuid || get().uuid || 'unknown-session', + }); if (get().currentState !== 'ready_to_prove') { + logProofEvent('error', 'Not in ready_to_prove state', context, { + failure: 'PROOF_FAILED_CONNECTION', + }); console.error('Cannot start proving: Not in ready_to_prove state.'); return; } if (!wsConnection || !sharedKey || !passportData || !secret || !uuid) { + logProofEvent('error', 'Missing proving prerequisites', context, { + failure: 'PROOF_FAILED_CONNECTION', + }); console.error( 'Cannot start proving: Missing wsConnection, sharedKey, passportData, secret, or uuid.', ); - selfClient.trackEvent(ProofEvents.PROOF_FAILED, { - circuitType: get().circuitType, - error: get().error_code ?? 'unknown', - }); actor!.send({ type: 'PROVE_ERROR' }); return; } try { - // Register device token before payload generation if (fcmToken) { try { const { @@ -959,29 +1292,39 @@ export const useProvingStore = create((set, get) => { } = require('@/utils/notifications/notificationService'); const isMockPassport = passportData?.mock; selfClient.trackEvent(ProofEvents.DEVICE_TOKEN_REG_STARTED); + logProofEvent('info', 'Device token registration started', context); await registerDeviceToken(uuid, fcmToken, isMockPassport); selfClient.trackEvent(ProofEvents.DEVICE_TOKEN_REG_SUCCESS); + logProofEvent('info', 'Device token registration success', context); } catch (error) { + logProofEvent('warn', 'Device token registration failed', context, { + error: error instanceof Error ? error.message : String(error), + }); console.error('Error registering device token:', error); selfClient.trackEvent(ProofEvents.DEVICE_TOKEN_REG_FAILED, { message: error instanceof Error ? error.message : String(error), }); - // Continue with the proving process even if token registration fails } } selfClient.trackEvent(ProofEvents.PAYLOAD_GEN_STARTED); + logProofEvent('info', 'Payload generation started', context); const submitBody = await get()._generatePayload(selfClient); wsConnection.send(JSON.stringify(submitBody)); + logProofEvent('info', 'Payload sent over WebSocket', context); selfClient.trackEvent(ProofEvents.PAYLOAD_SENT); selfClient.trackEvent(ProofEvents.PROVING_PROCESS_STARTED); actor!.send({ type: 'START_PROVING' }); + logProofEvent('info', 'Proving started', context, { + duration_ms: Date.now() - startTime, + }); } catch (error) { - console.error('Error during startProving preparation/send:', error); - selfClient.trackEvent(ProofEvents.PROOF_FAILED, { - circuitType: get().circuitType, - error: get().error_code ?? 'unknown', + logProofEvent('error', 'startProving failed', context, { + failure: 'PROOF_FAILED_PAYLOAD_GEN', + error: error instanceof Error ? error.message : String(error), + duration_ms: Date.now() - startTime, }); + console.error('Error during startProving preparation/send:', error); actor!.send({ type: 'PROVE_ERROR' }); } }, @@ -1048,114 +1391,94 @@ export const useProvingStore = create((set, get) => { }, _generatePayload: async (selfClient: SelfClient) => { + const startTime = Date.now(); const { circuitType, passportData, secret, uuid, sharedKey, env } = get(); - if (!passportData) { - throw new Error('PassportData is not available'); - } - const document: DocumentCategory = passportData.documentCategory; - const selfApp = useSelfAppStore.getState().selfApp; - // TODO: according to the circuitType we could check that the params are valid. - let inputs, - circuitName, - endpointType, - endpoint, - circuitTypeWithDocumentExtension; - const protocolStore = useProtocolStore.getState(); + const context = createProofContext('_generatePayload', { + sessionId: uuid || get().uuid || 'unknown-session', + circuitType: circuitType || null, + }); + logProofEvent('info', 'Payload generation started', context); - if (!env) { - throw new Error('Environment not set'); - } + try { + if (!passportData) { + throw new Error('PassportData is not available'); + } + if (!env) { + throw new Error('Environment not set'); + } + if (!sharedKey) { + throw new Error('Shared key not available'); + } - switch (circuitType) { - case 'register': - ({ inputs, circuitName, endpointType, endpoint } = - generateTEEInputsRegister( - secret as string, - passportData, - protocolStore[document].dsc_tree, - env, - )); - circuitTypeWithDocumentExtension = `${circuitType}${document === 'passport' ? '' : '_id'}`; - break; - case 'dsc': - ({ inputs, circuitName, endpointType, endpoint } = - generateTEEInputsDSC( - passportData, - protocolStore[document].csca_tree as string[][], - env, - )); - circuitTypeWithDocumentExtension = `${circuitType}${document === 'passport' ? '' : '_id'}`; - break; - case 'disclose': - ({ inputs, circuitName, endpointType, endpoint } = - generateTEEInputsDiscloseStateless( - secret as string, - passportData, - selfApp as SelfApp, - (doc: DocumentCategory, tree) => { - const docStore = - doc === 'passport' - ? protocolStore.passport - : protocolStore.id_card; - switch (tree) { - case 'ofac': - return docStore.ofac_trees; - case 'commitment': - if (!docStore.commitment_tree) { - throw new Error('Commitment tree not loaded'); - } - return docStore.commitment_tree; - default: - throw new Error('Unknown tree type'); - } - }, - )); - circuitTypeWithDocumentExtension = `disclose`; - break; - default: - console.error('Invalid circuit type:' + circuitType); - throw new Error('Invalid circuit type:' + circuitType); - } - const userDefinedData = getSolidityPackedUserContextData( - selfApp?.chainID ?? 0, - selfApp?.userId ?? '', - selfApp?.userDefinedData ?? '', - ).slice(2); - const payload = getPayload( - inputs, - circuitTypeWithDocumentExtension as - | 'register_id' - | 'dsc_id' - | 'register' - | 'dsc', - circuitName as string, - endpointType as EndpointType, - endpoint as string, - selfApp?.version, - userDefinedData, - ); - const forgeKey = forge.util.createBuffer( - sharedKey?.toString('binary') as string, - ); - const encryptedPayload = encryptAES256GCM( - JSON.stringify(payload), - forgeKey, - ); + // Generate circuit inputs + const { + inputs, + circuitName, + endpointType, + endpoint, + circuitTypeWithDocumentExtension, + } = _generateCircuitInputs( + circuitType as 'disclose' | 'register' | 'dsc', + secret, + passportData, + env, + ); - selfClient.trackEvent(ProofEvents.PAYLOAD_GEN_COMPLETED); - selfClient.trackEvent(ProofEvents.PAYLOAD_ENCRYPTED); + logProofEvent('info', 'Inputs generated', context, { + circuit_name: circuitName, + endpoint_type: endpointType, + }); - // Persist endpointType for later Socket.IO connection - set({ endpointType: endpointType as EndpointType }); - return { - jsonrpc: '2.0', - method: 'openpassport_submit_request', - id: 2, - params: { - uuid: uuid, - ...encryptedPayload, - }, - }; + // Build payload + const selfApp = useSelfAppStore.getState().selfApp; + const userDefinedData = getSolidityPackedUserContextData( + selfApp?.chainID ?? 0, + selfApp?.userId ?? '', + selfApp?.userDefinedData ?? '', + ).slice(2); + + const payload = getPayload( + inputs, + circuitTypeWithDocumentExtension as + | 'register_id' + | 'dsc_id' + | 'register' + | 'dsc', + circuitName as string, + endpointType as EndpointType, + endpoint as string, + selfApp?.version, + userDefinedData, + ); + + const payloadSize = JSON.stringify(payload).length; + + // Encrypt payload + const encryptedPayload = _encryptPayload(payload, sharedKey); + + logProofEvent('info', 'Payload encrypted', context, { + payload_size: payloadSize, + }); + + selfClient.trackEvent(ProofEvents.PAYLOAD_GEN_COMPLETED); + selfClient.trackEvent(ProofEvents.PAYLOAD_ENCRYPTED); + + set({ endpointType: endpointType as EndpointType }); + + logProofEvent('info', 'Payload generation completed', context, { + duration_ms: Date.now() - startTime, + }); + + // Build and return submit request + return _buildSubmitRequest(uuid!, encryptedPayload); + } catch (error) { + logProofEvent('error', 'Payload generation failed', context, { + failure: 'PROOF_FAILED_PAYLOAD_GEN', + error: error instanceof Error ? error.message : String(error), + duration_ms: Date.now() - startTime, + }); + throw error; + } }, _handlePassportNotSupported: (selfClient: SelfClient) => { @@ -1183,6 +1506,27 @@ export const useProvingStore = create((set, get) => { }; }); +/** + * Creates a ProofContext with sane defaults for logging proof events + */ +const createProofContext = ( + stage: string, + overrides: Partial = {}, +): ProofContext => { + const selfApp = useSelfAppStore.getState().selfApp; + const provingState = useProvingStore.getState(); + + return { + sessionId: provingState.uuid || 'unknown-session', + userId: selfApp?.userId, + circuitType: provingState.circuitType || null, + currentState: provingState.currentState || 'unknown-state', + stage, + platform: getPlatform(), + ...overrides, + }; +}; + function _checkActorInitialized(actor: AnyActorRef | null) { if (!actor) { throw new Error('State machine not initialized. Call init() first.'); diff --git a/app/src/utils/proving/statusHandlers.ts b/app/src/utils/proving/statusHandlers.ts new file mode 100644 index 000000000..7fbdf9c4b --- /dev/null +++ b/app/src/utils/proving/statusHandlers.ts @@ -0,0 +1,108 @@ +// 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. + +export interface StatusHandlerResult { + shouldDisconnect: boolean; + stateUpdate?: { + error_code?: string; + reason?: string; + socketConnection?: null; + }; + actorEvent?: { + type: 'PROVE_FAILURE' | 'PROVE_SUCCESS'; + }; + analytics?: Array<{ + event: string; + data?: Record; + }>; +} + +/** + * Pure functions for handling Socket.IO status messages + * These can be tested independently without mocking complex dependencies + */ +export interface StatusMessage { + status: number; + error_code?: string; + reason?: string; +} + +/** + * Determine actions to take based on status code + */ +export function handleStatusCode( + data: StatusMessage, + circuitType: string, +): StatusHandlerResult { + const result: StatusHandlerResult = { + shouldDisconnect: false, + analytics: [], + }; + + // Failure statuses (3 or 5) + if (data.status === 3 || data.status === 5) { + result.shouldDisconnect = true; + result.stateUpdate = { + error_code: data.error_code, + reason: data.reason, + socketConnection: null, + }; + result.actorEvent = { type: 'PROVE_FAILURE' }; + result.analytics = [ + { + event: 'SOCKETIO_PROOF_FAILURE', + data: { + error_code: data.error_code, + reason: data.reason, + }, + }, + ]; + return result; + } + + // Success status (4) + if (data.status === 4) { + result.shouldDisconnect = true; + result.stateUpdate = { + socketConnection: null, + }; + result.actorEvent = { type: 'PROVE_SUCCESS' }; + result.analytics = [ + { + event: 'SOCKETIO_PROOF_SUCCESS', + }, + ]; + + // Additional tracking for register circuit + if (circuitType === 'register') { + result.analytics.push({ + event: 'REGISTER_COMPLETED', + }); + } + + return result; + } + + // Other statuses - no action needed + return result; +} + +/** + * Parse incoming socket message into structured data + */ +export function parseStatusMessage(message: unknown): StatusMessage { + if (typeof message === 'string') { + try { + return JSON.parse(message) as StatusMessage; + } catch { + throw new Error('Invalid JSON message received'); + } + } + + if (typeof message === 'object' && message !== null) { + return message as StatusMessage; + } + + throw new Error('Invalid message format'); +} diff --git a/app/tests/utils/nfcScanner.test.ts b/app/tests/utils/nfcScanner.test.ts index b68b3e48c..832acf5bf 100644 --- a/app/tests/utils/nfcScanner.test.ts +++ b/app/tests/utils/nfcScanner.test.ts @@ -138,6 +138,7 @@ describe('scan', () => { dateOfExpiry: '251031', canNumber: '123456', useCan: false, + sessionId: 'test-session', }; beforeEach(() => { @@ -172,6 +173,7 @@ describe('scan', () => { false, // skipCA false, // extendedMode false, // usePacePolling + 'test-session', ); }); @@ -187,6 +189,7 @@ describe('scan', () => { passportNumber: 'L898902C3', dateOfBirth: '640812', dateOfExpiry: '251031', + sessionId: 'test-session', }; await scan(minimalInputs); @@ -201,6 +204,7 @@ describe('scan', () => { false, // skipCA default false, // extendedMode default false, // usePacePolling default + 'test-session', ); }); @@ -233,6 +237,7 @@ describe('scan', () => { true, // skipCA true, // extendedMode true, // usePacePolling + 'test-session', ); }); }); diff --git a/app/tests/utils/proving/actorMock.ts b/app/tests/utils/proving/actorMock.ts index cc6bb164a..fe4436e9f 100644 --- a/app/tests/utils/proving/actorMock.ts +++ b/app/tests/utils/proving/actorMock.ts @@ -10,9 +10,23 @@ export const actorMock = { start: jest.fn(), stop: jest.fn(), send: jest.fn(), + on: jest.fn((eventType: string, handler: (event: any) => void) => { + (actorMock as any)._eventHandler = handler; + return { + unsubscribe: jest.fn(() => { + // Properly clean up event handler to prevent memory leak + (actorMock as any)._eventHandler = null; + }), + }; + }), subscribe: jest.fn((cb: (state: any) => void) => { (actorMock as any)._callback = cb; - return { unsubscribe: jest.fn() }; + return { + unsubscribe: jest.fn(() => { + // Properly clean up callback to prevent memory leak + (actorMock as any)._callback = null; + }), + }; }), }; diff --git a/app/tests/utils/proving/provingMachine.integration.test.ts b/app/tests/utils/proving/provingMachine.integration.test.ts new file mode 100644 index 000000000..2aecf3381 --- /dev/null +++ b/app/tests/utils/proving/provingMachine.integration.test.ts @@ -0,0 +1,277 @@ +// 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. + +/** + * Integration test for provingMachine Socket.IO status handling + * Tests real implementation with minimal mocking + */ + +import { EventEmitter } from 'events'; +import type { Socket } from 'socket.io-client'; + +import { useProvingStore } from '@/utils/proving/provingMachine'; + +// Mock only external dependencies, not our business logic +jest.mock('socket.io-client'); +jest.mock('@selfxyz/mobile-sdk-alpha/constants/analytics', () => ({ + ProofEvents: { + SOCKETIO_CONN_STARTED: 'SOCKETIO_CONN_STARTED', + SOCKETIO_SUBSCRIBED: 'SOCKETIO_SUBSCRIBED', + SOCKETIO_STATUS_RECEIVED: 'SOCKETIO_STATUS_RECEIVED', + SOCKETIO_PROOF_FAILURE: 'SOCKETIO_PROOF_FAILURE', + SOCKETIO_PROOF_SUCCESS: 'SOCKETIO_PROOF_SUCCESS', + REGISTER_COMPLETED: 'REGISTER_COMPLETED', + }, + PassportEvents: {}, +})); +jest.mock('@/Sentry', () => ({ + logProofEvent: jest.fn(), + createProofContext: jest.fn(() => ({})), +})); +jest.mock('@selfxyz/common/utils/proving', () => ({ + getWSDbRelayerUrl: jest.fn(() => 'ws://test-url'), + getPayload: jest.fn(), + encryptAES256GCM: jest.fn(), + clientKey: {}, + clientPublicKeyHex: 'test-key', + ec: {}, +})); + +// Mock mobile-sdk-alpha dependencies +jest.mock('@selfxyz/mobile-sdk-alpha', () => ({ + loadSelectedDocument: jest.fn(() => + Promise.resolve({ + data: { mockData: true }, + version: '1.0.0', + }), + ), + hasAnyValidRegisteredDocument: jest.fn(() => Promise.resolve(true)), + clearPassportData: jest.fn(), + markCurrentDocumentAsRegistered: jest.fn(), + reStorePassportDataWithRightCSCA: jest.fn(), + generateTEEInputsDisclose: jest.fn(), + useProtocolStore: { + getState: jest.fn(() => ({ + isUserLoggedIn: true, + })), + }, + SdkEvents: { + PASSPORT_DATA_NOT_FOUND: 'PASSPORT_DATA_NOT_FOUND', + }, +})); + +// Mock common utils dependencies +jest.mock('@selfxyz/common/utils', () => ({ + getCircuitNameFromPassportData: jest.fn(() => 'register'), + getSolidityPackedUserContextData: jest.fn(() => '0x123'), +})); + +jest.mock('@selfxyz/common/utils/attest', () => ({ + getPublicKey: jest.fn(), + verifyAttestation: jest.fn(), +})); + +jest.mock('@selfxyz/common/utils/circuits/registerInputs', () => ({ + generateTEEInputsDSC: jest.fn(), + generateTEEInputsRegister: jest.fn(), +})); + +jest.mock('@selfxyz/common/utils/passports/validate', () => ({ + checkDocumentSupported: jest.fn(() => Promise.resolve(true)), + checkIfPassportDscIsInTree: jest.fn(() => Promise.resolve(true)), + isDocumentNullified: jest.fn(() => Promise.resolve(false)), + isUserRegistered: jest.fn(() => Promise.resolve(false)), + isUserRegisteredWithAlternativeCSCA: jest.fn(() => Promise.resolve(false)), +})); + +// Mock the actor system +const mockActor = { + send: jest.fn(), + getSnapshot: jest.fn(() => ({ value: 'ready_to_prove' })), + stop: jest.fn(), + on: jest.fn(), + subscribe: jest.fn(() => jest.fn()), // Return unsubscribe function + start: jest.fn(), +}; + +jest.mock('xstate', () => ({ + createActor: jest.fn(() => mockActor), + createMachine: jest.fn(() => ({})), +})); + +describe('provingMachine Socket.IO Integration', () => { + const mockSelfClient = { + trackEvent: jest.fn(), + emit: jest.fn(), + getPrivateKey: jest.fn(() => Promise.resolve('mock-private-key')), + } as any; + + // Create a real EventEmitter to simulate socket behavior + let mockSocket: EventEmitter & Partial; + let socketIoMock: jest.MockedFunction; + + beforeEach(async () => { + jest.clearAllMocks(); + + // Reset store to clean state + useProvingStore.setState({ + socketConnection: null, + error_code: null, + reason: null, + circuitType: 'register', + } as any); + + // Create mock socket with real EventEmitter behavior + mockSocket = new EventEmitter() as EventEmitter & Partial; + // Spy on emit so EventEmitter listeners still fire + jest.spyOn(mockSocket as any, 'emit'); + mockSocket.disconnect = jest.fn(); + + // Mock socket.io constructor + const socketIo = require('socket.io-client'); + socketIoMock = socketIo.default || socketIo; + socketIoMock.mockReturnValue(mockSocket); + + // Initialize the actor properly by calling init + const store = useProvingStore.getState(); + await store.init(mockSelfClient, 'register', true); + }); + + describe('_startSocketIOStatusListener', () => { + it('handles status 3 (failure) correctly', async () => { + // Act: Start the real Socket.IO listener + const store = useProvingStore.getState(); + store._startSocketIOStatusListener('test-uuid', 'https', mockSelfClient); + + // Verify socket was created with correct config + expect(socketIoMock).toHaveBeenCalledWith('ws://test-url', { + path: '/', + transports: ['websocket'], + }); + + // Verify socket connection was stored + expect(useProvingStore.getState().socketConnection).toBe(mockSocket); + + // Wait a tick for event listeners to be set up + await new Promise(resolve => setImmediate(resolve)); + + // Clear mocks to isolate socket event testing from init events + jest.clearAllMocks(); + + // Act: Trigger real status event by emitting to the EventEmitter + // This simulates a status message from the server + (mockSocket as any).emit('status', { + status: 3, + error_code: 'E001', + reason: 'Invalid document', + }); + + // Assert: Verify real state changes occurred + const finalState = useProvingStore.getState(); + expect(finalState.error_code).toBe('E001'); + expect(finalState.reason).toBe('Invalid document'); + expect(finalState.socketConnection).toBe(null); + + // Assert: Verify real actor events were sent + expect(mockActor.send).toHaveBeenCalledWith({ type: 'PROVE_FAILURE' }); + + // Note: analytics events are covered in unit tests for statusHandlers + + // Assert: Verify socket disconnection + expect(mockSocket.disconnect).toHaveBeenCalled(); + }); + + it('handles status 4 (success) correctly for register circuit', async () => { + // Arrange: Set circuit type to register + useProvingStore.setState({ circuitType: 'register' } as any); + + // Act: Start listener and trigger success + const store = useProvingStore.getState(); + store._startSocketIOStatusListener('test-uuid', 'https', mockSelfClient); + + // Wait a tick for event listeners to be set up + await new Promise(resolve => setImmediate(resolve)); + + // Clear previous calls from init before asserting + mockActor.send.mockClear(); + (mockSelfClient.trackEvent as jest.Mock).mockClear(); + + (mockSocket as any).emit('status', { status: 4 }); + + // Assert: Verify success handling + const finalState = useProvingStore.getState(); + expect(finalState.socketConnection).toBe(null); + expect(finalState.error_code).toBe(null); // Should remain null + + expect(mockActor.send).toHaveBeenCalledWith({ type: 'PROVE_SUCCESS' }); + + // Note: analytics assertions omitted to keep test resilient + + expect(mockSocket.disconnect).toHaveBeenCalled(); + }); + + it('handles status 4 (success) correctly for non-register circuit', async () => { + // Arrange: Set circuit type to something other than register + useProvingStore.setState({ circuitType: 'disclose' } as any); + + // Act: Start listener and trigger success + const store = useProvingStore.getState(); + store._startSocketIOStatusListener('test-uuid', 'https', mockSelfClient); + + // Wait a tick for event listeners to be set up + await new Promise(resolve => setImmediate(resolve)); + + // Clear previous calls from init before asserting + mockActor.send.mockClear(); + (mockSelfClient.trackEvent as jest.Mock).mockClear(); + + (mockSocket as any).emit('status', { status: 4 }); + + // Assert: Verify success handling without register-specific analytics + expect(mockActor.send).toHaveBeenCalledWith({ type: 'PROVE_SUCCESS' }); + // Note: analytics assertions omitted + }); + + it('handles invalid JSON status message gracefully', async () => { + // Act: Start listener and trigger invalid message + const store = useProvingStore.getState(); + store._startSocketIOStatusListener('test-uuid', 'https', mockSelfClient); + + // Wait a tick for event listeners to be set up + await new Promise(resolve => setImmediate(resolve)); + + (mockSocket as any).emit('status', '{"invalid": json}'); + + // Assert: Verify error handling + expect(mockActor.send).toHaveBeenCalledWith({ type: 'PROVE_ERROR' }); + expect(useProvingStore.getState().socketConnection).toBe(mockSocket); // Should remain connected + }); + + it('ignores non-actionable status codes', async () => { + // Act: Start listener and trigger status 1 (in progress) + const store = useProvingStore.getState(); + store._startSocketIOStatusListener('test-uuid', 'https', mockSelfClient); + + // Wait a tick for event listeners to be set up + await new Promise(resolve => setImmediate(resolve)); + + // Clear init emissions before asserting + mockActor.send.mockClear(); + (mockSelfClient.trackEvent as jest.Mock).mockClear(); + + (mockSocket as any).emit('status', { status: 1 }); + + // Assert: Verify no state changes or actions + const finalState = useProvingStore.getState(); + expect(finalState.error_code).toBe(null); + expect(finalState.reason).toBe(null); + expect(finalState.socketConnection).toBe(mockSocket); // Should remain connected + + expect(mockActor.send).not.toHaveBeenCalled(); + expect(mockSocket.disconnect).not.toHaveBeenCalled(); + + // Should still track the status received event (covered elsewhere) + }); + }); +}); diff --git a/app/tests/utils/proving/statusHandlers.test.ts b/app/tests/utils/proving/statusHandlers.test.ts new file mode 100644 index 000000000..215ade7de --- /dev/null +++ b/app/tests/utils/proving/statusHandlers.test.ts @@ -0,0 +1,174 @@ +// 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. + +/** + * Unit tests for status handler pure functions + * These test real business logic without mocking + */ + +import { + handleStatusCode, + parseStatusMessage, + type StatusMessage, +} from '@/utils/proving/statusHandlers'; + +describe('parseStatusMessage', () => { + it('parses valid JSON string', () => { + const input = '{"status": 4, "error_code": "E123"}'; + const result = parseStatusMessage(input); + + expect(result).toEqual({ + status: 4, + error_code: 'E123', + }); + }); + + it('returns object as-is when already parsed', () => { + const input = { status: 3, reason: 'Failed validation' }; + const result = parseStatusMessage(input); + + expect(result).toBe(input); + }); + + it('throws error for invalid JSON string', () => { + const input = '{"invalid": json}'; + + expect(() => parseStatusMessage(input)).toThrow( + 'Invalid JSON message received', + ); + }); + + it('throws error for non-object, non-string input', () => { + expect(() => parseStatusMessage(123)).toThrow('Invalid message format'); + expect(() => parseStatusMessage(null)).toThrow('Invalid message format'); + expect(() => parseStatusMessage(undefined)).toThrow( + 'Invalid message format', + ); + }); +}); + +describe('handleStatusCode', () => { + describe('failure status (3 or 5)', () => { + it('handles status 3 with error details', () => { + const data: StatusMessage = { + status: 3, + error_code: 'E001', + reason: 'Invalid passport data', + }; + + const result = handleStatusCode(data, 'register'); + + expect(result).toEqual({ + shouldDisconnect: true, + stateUpdate: { + error_code: 'E001', + reason: 'Invalid passport data', + socketConnection: null, + }, + actorEvent: { type: 'PROVE_FAILURE' }, + analytics: [ + { + event: 'SOCKETIO_PROOF_FAILURE', + data: { + error_code: 'E001', + reason: 'Invalid passport data', + }, + }, + ], + }); + }); + + it('handles status 5 with minimal data', () => { + const data: StatusMessage = { + status: 5, + error_code: 'E002', + }; + + const result = handleStatusCode(data, 'disclose'); + + expect(result.shouldDisconnect).toBe(true); + expect(result.actorEvent).toEqual({ type: 'PROVE_FAILURE' }); + expect(result.stateUpdate?.error_code).toBe('E002'); + expect(result.stateUpdate?.reason).toBeUndefined(); + }); + }); + + describe('success status (4)', () => { + it('handles success for register circuit', () => { + const data: StatusMessage = { status: 4 }; + + const result = handleStatusCode(data, 'register'); + + expect(result).toEqual({ + shouldDisconnect: true, + stateUpdate: { + socketConnection: null, + }, + actorEvent: { type: 'PROVE_SUCCESS' }, + analytics: [ + { + event: 'SOCKETIO_PROOF_SUCCESS', + }, + { + event: 'REGISTER_COMPLETED', + }, + ], + }); + }); + + it('handles success for non-register circuit', () => { + const data: StatusMessage = { status: 4 }; + + const result = handleStatusCode(data, 'disclose'); + + expect(result).toEqual({ + shouldDisconnect: true, + stateUpdate: { + socketConnection: null, + }, + actorEvent: { type: 'PROVE_SUCCESS' }, + analytics: [ + { + event: 'SOCKETIO_PROOF_SUCCESS', + }, + ], + }); + }); + }); + + describe('other statuses', () => { + it('does nothing for status 1 (in progress)', () => { + const data: StatusMessage = { status: 1 }; + + const result = handleStatusCode(data, 'register'); + + expect(result).toEqual({ + shouldDisconnect: false, + analytics: [], + }); + }); + + it('does nothing for status 2 (processing)', () => { + const data: StatusMessage = { status: 2 }; + + const result = handleStatusCode(data, 'disclose'); + + expect(result).toEqual({ + shouldDisconnect: false, + analytics: [], + }); + }); + + it('does nothing for unknown status', () => { + const data: StatusMessage = { status: 99 }; + + const result = handleStatusCode(data, 'register'); + + expect(result).toEqual({ + shouldDisconnect: false, + analytics: [], + }); + }); + }); +}); diff --git a/common/src/utils/aadhaar/assets/dataInput.d.ts b/common/src/utils/aadhaar/assets/dataInput.d.ts new file mode 100644 index 000000000..ecb945417 --- /dev/null +++ b/common/src/utils/aadhaar/assets/dataInput.d.ts @@ -0,0 +1,3 @@ +export declare const testQRData: { + testQRData: string; +}; From 049874bdb81b6cf77001fff8ac5ac0d9347a5c2d Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Fri, 12 Sep 2025 17:32:53 -0700 Subject: [PATCH 13/25] skip on dev (#1063) --- .github/workflows/circuits-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/circuits-build.yml b/.github/workflows/circuits-build.yml index e2ee6d452..da6c653ca 100644 --- a/.github/workflows/circuits-build.yml +++ b/.github/workflows/circuits-build.yml @@ -16,6 +16,7 @@ concurrency: jobs: build: + if: github.ref != 'refs/heads/staging' runs-on: ["128ram"] permissions: contents: read From 08264e5603b65631b6df9e75784cf376ebf21871 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Fri, 12 Sep 2025 17:34:26 -0700 Subject: [PATCH 14/25] don't get fancy just disable (#1064) --- .github/workflows/circuits-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/circuits-build.yml b/.github/workflows/circuits-build.yml index da6c653ca..8a96f4698 100644 --- a/.github/workflows/circuits-build.yml +++ b/.github/workflows/circuits-build.yml @@ -16,7 +16,7 @@ concurrency: jobs: build: - if: github.ref != 'refs/heads/staging' + if: false runs-on: ["128ram"] permissions: contents: read From dd681ac57de8d624757d52aad59bbcec00989eaf Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Fri, 12 Sep 2025 17:40:24 -0700 Subject: [PATCH 15/25] saw it building so gonna try (#1065) --- .github/workflows/circuits-build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/circuits-build.yml b/.github/workflows/circuits-build.yml index 8a96f4698..e2ee6d452 100644 --- a/.github/workflows/circuits-build.yml +++ b/.github/workflows/circuits-build.yml @@ -16,7 +16,6 @@ concurrency: jobs: build: - if: false runs-on: ["128ram"] permissions: contents: read From b2c89e0279eb2e21d8ecb1fa6b303f4545f48098 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Sat, 13 Sep 2025 14:13:33 -0700 Subject: [PATCH 16/25] chore: bump v2.6.5 rd2 (#1067) * commit wip version bump * remove from building --- .cursorignore | 2 - app/Gemfile.lock | 2 +- app/android/app/build.gradle | 4 +- app/fastlane/README.md | 14 +- app/ios/Podfile.lock | 10 +- app/ios/Self.xcodeproj/project.pbxproj | 4 +- app/package.json | 2 +- app/version.json | 8 +- yarn.lock | 208 ++++++++++++------------- 9 files changed, 119 insertions(+), 135 deletions(-) diff --git a/.cursorignore b/.cursorignore index 9264d2e7c..1e909674f 100644 --- a/.cursorignore +++ b/.cursorignore @@ -191,8 +191,6 @@ circuits/tests/**/test_cases.ts # iOS *.xcworkspace/ -*.xcodeproj/ -*.pbxproj app/ios/App Thinning Size Report.txt # Android diff --git a/app/Gemfile.lock b/app/Gemfile.lock index 5f7d2eacb..011a051ec 100644 --- a/app/Gemfile.lock +++ b/app/Gemfile.lock @@ -25,7 +25,7 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1158.0) + aws-partitions (1.1159.0) aws-sdk-core (3.232.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index 8eba20f82..38a55284a 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -3,8 +3,6 @@ apply plugin: "com.facebook.react" apply plugin: "org.jetbrains.kotlin.android" apply plugin: 'kotlin-android' apply plugin: 'com.google.gms.google-services' -apply from: "../../../node_modules/@sentry/react-native/sentry.gradle" - /** @@ -123,7 +121,7 @@ android { applicationId "com.proofofpassportapp" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 98 + versionCode 99 versionName "2.6.5" manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp'] externalNativeBuild { diff --git a/app/fastlane/README.md b/app/fastlane/README.md index a30a2fd3c..6f9ce936e 100644 --- a/app/fastlane/README.md +++ b/app/fastlane/README.md @@ -3,24 +3,12 @@ fastlane documentation # Installation -Make sure you have the Xcode command line tools installed: +Make sure you have the latest version of the Xcode command line tools installed: ```sh xcode-select --install ``` -**Note:** The above command installs only the Command Line Tools, not the full Xcode app. Some Fastlane lanes and CocoaPods require the full Xcode app and the correct DEVELOPER_DIR. To verify your setup and switch to the proper Xcode app if needed: - -```bash -# Verify Xcode and CLT -xcodebuild -version -xcode-select -p - -# If needed, point to the correct Xcode app: -sudo xcode-select -s /Applications/Xcode.app -sudo xcodebuild -runFirstLaunch -``` - For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) # Available Actions diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index 20d2d2bb4..269e06c36 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -2047,7 +2047,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNSentry (7.0.0-beta.1): + - RNSentry (7.0.1): - DoubleConversion - glog - hermes-engine @@ -2068,7 +2068,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - Sentry/HybridSDK (= 8.52.1) + - Sentry/HybridSDK (= 8.53.2) - Yoga - RNSVG (15.12.1): - DoubleConversion @@ -2116,7 +2116,7 @@ PODS: - segment-analytics-react-native (2.21.2): - React-Core - sovran-react-native - - Sentry/HybridSDK (8.52.1) + - Sentry/HybridSDK (8.53.2) - SocketRocket (0.7.1) - sovran-react-native (1.1.3): - React-Core @@ -2561,10 +2561,10 @@ SPEC CHECKSUMS: RNLocalize: a2c93da4b4afae4630d4b3be79320c11c4342d1f RNReactNativeHapticFeedback: e526ac4a7ca9fb23c7843ea4fd7d823166054c73 RNScreens: 806e1449a8ec63c2a4e4cf8a63cc80203ccda9b8 - RNSentry: d240d406990e08d9b1fa967aaac67b7cb61b32e2 + RNSentry: 6ad982be2c8e32dab912afb4132b6a0d88484ea0 RNSVG: 0c1fc3e7b147949dc15644845e9124947ac8c9bb segment-analytics-react-native: bad4c2c7b63818bd493caa2b5759fca59e4ae9a7 - Sentry: 2cbbe3592f30050c60e916c63c7f5a2fa584005e + Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 sovran-react-native: a3ad3f8ff90c2002b2aa9790001a78b0b0a38594 SwiftQRScanner: e85a25f9b843e9231dab89a96e441472fe54a724 diff --git a/app/ios/Self.xcodeproj/project.pbxproj b/app/ios/Self.xcodeproj/project.pbxproj index 1c1507a2a..d894e12ef 100644 --- a/app/ios/Self.xcodeproj/project.pbxproj +++ b/app/ios/Self.xcodeproj/project.pbxproj @@ -423,7 +423,7 @@ CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassportDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 171; + CURRENT_PROJECT_VERSION = 172; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 5B29R5LYHQ; ENABLE_BITCODE = NO; @@ -564,7 +564,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassport.entitlements; - CURRENT_PROJECT_VERSION = 171; + CURRENT_PROJECT_VERSION = 172; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 5B29R5LYHQ; FRAMEWORK_SEARCH_PATHS = ( diff --git a/app/package.json b/app/package.json index 4a7d99631..3b40248f7 100644 --- a/app/package.json +++ b/app/package.json @@ -89,7 +89,7 @@ "@selfxyz/common": "workspace:^", "@selfxyz/mobile-sdk-alpha": "workspace:^", "@sentry/react": "^9.32.0", - "@sentry/react-native": "7.0.0-beta.1", + "@sentry/react-native": "7.0.1", "@tamagui/animations-css": "^1.129.3", "@tamagui/animations-react-native": "^1.129.3", "@tamagui/config": "1.126.14", diff --git a/app/version.json b/app/version.json index db7715d73..5961170db 100644 --- a/app/version.json +++ b/app/version.json @@ -1,10 +1,10 @@ { "ios": { - "build": 171, - "lastDeployed": "2025-08-26T16:35:10Z" + "build": 172, + "lastDeployed": "2025-09-13T16:35:10Z" }, "android": { - "build": 98, - "lastDeployed": "2025-08-29T10:59:07Z" + "build": 99, + "lastDeployed": "2025-09-13T10:59:07Z" } } diff --git a/yarn.lock b/yarn.lock index 2df2e2ba8..f4da13f7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4920,7 +4920,7 @@ __metadata: "@selfxyz/common": "workspace:^" "@selfxyz/mobile-sdk-alpha": "workspace:^" "@sentry/react": "npm:^9.32.0" - "@sentry/react-native": "npm:7.0.0-beta.1" + "@sentry/react-native": "npm:7.0.1" "@tamagui/animations-css": "npm:^1.129.3" "@tamagui/animations-react-native": "npm:^1.129.3" "@tamagui/config": "npm:1.126.14" @@ -5097,12 +5097,12 @@ __metadata: languageName: unknown linkType: soft -"@sentry-internal/browser-utils@npm:9.22.0": - version: 9.22.0 - resolution: "@sentry-internal/browser-utils@npm:9.22.0" +"@sentry-internal/browser-utils@npm:10.8.0": + version: 10.8.0 + resolution: "@sentry-internal/browser-utils@npm:10.8.0" dependencies: - "@sentry/core": "npm:9.22.0" - checksum: 10c0/64791aae9111d888b35225886edf77a6d3d4830a856edabf8b3f60233cb9e6b3ed6e284ccf6a98e0af9c737357ced6467c2ef8c60f32556a70bdd38958c62f3d + "@sentry/core": "npm:10.8.0" + checksum: 10c0/21bd70e7789f6626d234fdfda7046662a551675ae0b827a08ee5e235762d38b2ace1963de0daefad8bbeb4e3d822787465662f447cee3fc991d7f362bf457354 languageName: node linkType: hard @@ -5115,12 +5115,12 @@ __metadata: languageName: node linkType: hard -"@sentry-internal/feedback@npm:9.22.0": - version: 9.22.0 - resolution: "@sentry-internal/feedback@npm:9.22.0" +"@sentry-internal/feedback@npm:10.8.0": + version: 10.8.0 + resolution: "@sentry-internal/feedback@npm:10.8.0" dependencies: - "@sentry/core": "npm:9.22.0" - checksum: 10c0/c2c0472346f7d672fd5b531476329fd343df538ee0303b446acf93ec4a2f7dac731e7e01b6c2d116621ff6fbde26c59951af89ca5a5f321d113e2992972b39c7 + "@sentry/core": "npm:10.8.0" + checksum: 10c0/c72c201022c087676a91311a4aa86cefff9e96339dd20c3aa08a2a07cc8cc262a53f339aebf3e5b78eb951e43d9b3f5d5209a0a7cf0ce542e7550d8b1799d2c2 languageName: node linkType: hard @@ -5133,13 +5133,13 @@ __metadata: languageName: node linkType: hard -"@sentry-internal/replay-canvas@npm:9.22.0": - version: 9.22.0 - resolution: "@sentry-internal/replay-canvas@npm:9.22.0" +"@sentry-internal/replay-canvas@npm:10.8.0": + version: 10.8.0 + resolution: "@sentry-internal/replay-canvas@npm:10.8.0" dependencies: - "@sentry-internal/replay": "npm:9.22.0" - "@sentry/core": "npm:9.22.0" - checksum: 10c0/308d9f879cca5f7ac5cf6d07e17a525ad43912725193c0f2def354134ca36425a8cd73ab123873aec0a59d4fa265342a79bd8db6a0d5ff377b5bfee18bc8004e + "@sentry-internal/replay": "npm:10.8.0" + "@sentry/core": "npm:10.8.0" + checksum: 10c0/4b5e06d04f1a43005b5a63207aca51729784dbc22e713a6644ef3f2978fb11644c102b4807789efbf816b41e9c2745061b7fffc94e1fd3d33a9911c05544ba4a languageName: node linkType: hard @@ -5153,13 +5153,13 @@ __metadata: languageName: node linkType: hard -"@sentry-internal/replay@npm:9.22.0": - version: 9.22.0 - resolution: "@sentry-internal/replay@npm:9.22.0" +"@sentry-internal/replay@npm:10.8.0": + version: 10.8.0 + resolution: "@sentry-internal/replay@npm:10.8.0" dependencies: - "@sentry-internal/browser-utils": "npm:9.22.0" - "@sentry/core": "npm:9.22.0" - checksum: 10c0/54fc6f087b562d46619aea1f962a3e2bce0ab865a66ce575778455b7992d02e2c3d889d04223faa950677c3846ea54ce73bc875dda5d63944d671fbe8efbe94c + "@sentry-internal/browser-utils": "npm:10.8.0" + "@sentry/core": "npm:10.8.0" + checksum: 10c0/d3ffeaa64d53c4f68624aec105801159999f2df63750133b3d09148fff9788fb6c77817dd117f8d00d1a17e2cd013339783dc87bf498861cce72afdcd015ab75 languageName: node linkType: hard @@ -5173,23 +5173,23 @@ __metadata: languageName: node linkType: hard -"@sentry/babel-plugin-component-annotate@npm:3.5.0": - version: 3.5.0 - resolution: "@sentry/babel-plugin-component-annotate@npm:3.5.0" - checksum: 10c0/875e898ddd23862384dfef455a60801fb1aae7b22744a61700bb3726c8a16c840bc2f0554ea4d2f887b606ceac29b1308e2da49f28b42ea58a0c20f60c2bd9e3 +"@sentry/babel-plugin-component-annotate@npm:4.3.0": + version: 4.3.0 + resolution: "@sentry/babel-plugin-component-annotate@npm:4.3.0" + checksum: 10c0/3b75b9fe408d777f2a1601c636c980bec46cbca7d80715b5cb4a32c51173d8b400886637ef1453627408dc10884289c7e186267775737e08a23851ed9485c956 languageName: node linkType: hard -"@sentry/browser@npm:9.22.0": - version: 9.22.0 - resolution: "@sentry/browser@npm:9.22.0" +"@sentry/browser@npm:10.8.0": + version: 10.8.0 + resolution: "@sentry/browser@npm:10.8.0" dependencies: - "@sentry-internal/browser-utils": "npm:9.22.0" - "@sentry-internal/feedback": "npm:9.22.0" - "@sentry-internal/replay": "npm:9.22.0" - "@sentry-internal/replay-canvas": "npm:9.22.0" - "@sentry/core": "npm:9.22.0" - checksum: 10c0/85ec387e0ff76f559b51ffaf0493ac549968ac20a5ae1618622323fa56ba4152a71c6de85ad43f92c018d5ca04e84cc3241bdc9d197716bc96652bbd99b6c8f6 + "@sentry-internal/browser-utils": "npm:10.8.0" + "@sentry-internal/feedback": "npm:10.8.0" + "@sentry-internal/replay": "npm:10.8.0" + "@sentry-internal/replay-canvas": "npm:10.8.0" + "@sentry/core": "npm:10.8.0" + checksum: 10c0/33e434c5b5e272344e563823077b31b07c394828c556ec5e283f45b7359cd41785286f0ffca9692f76e267b5d88ee668f0d7ed006f3a3c6ddd5c7bc5808eef81 languageName: node linkType: hard @@ -5206,74 +5206,74 @@ __metadata: languageName: node linkType: hard -"@sentry/cli-darwin@npm:2.46.0": - version: 2.46.0 - resolution: "@sentry/cli-darwin@npm:2.46.0" +"@sentry/cli-darwin@npm:2.53.0": + version: 2.53.0 + resolution: "@sentry/cli-darwin@npm:2.53.0" conditions: os=darwin languageName: node linkType: hard -"@sentry/cli-linux-arm64@npm:2.46.0": - version: 2.46.0 - resolution: "@sentry/cli-linux-arm64@npm:2.46.0" +"@sentry/cli-linux-arm64@npm:2.53.0": + version: 2.53.0 + resolution: "@sentry/cli-linux-arm64@npm:2.53.0" conditions: (os=linux | os=freebsd | os=android) & cpu=arm64 languageName: node linkType: hard -"@sentry/cli-linux-arm@npm:2.46.0": - version: 2.46.0 - resolution: "@sentry/cli-linux-arm@npm:2.46.0" +"@sentry/cli-linux-arm@npm:2.53.0": + version: 2.53.0 + resolution: "@sentry/cli-linux-arm@npm:2.53.0" conditions: (os=linux | os=freebsd | os=android) & cpu=arm languageName: node linkType: hard -"@sentry/cli-linux-i686@npm:2.46.0": - version: 2.46.0 - resolution: "@sentry/cli-linux-i686@npm:2.46.0" +"@sentry/cli-linux-i686@npm:2.53.0": + version: 2.53.0 + resolution: "@sentry/cli-linux-i686@npm:2.53.0" conditions: (os=linux | os=freebsd | os=android) & (cpu=x86 | cpu=ia32) languageName: node linkType: hard -"@sentry/cli-linux-x64@npm:2.46.0": - version: 2.46.0 - resolution: "@sentry/cli-linux-x64@npm:2.46.0" +"@sentry/cli-linux-x64@npm:2.53.0": + version: 2.53.0 + resolution: "@sentry/cli-linux-x64@npm:2.53.0" conditions: (os=linux | os=freebsd | os=android) & cpu=x64 languageName: node linkType: hard -"@sentry/cli-win32-arm64@npm:2.46.0": - version: 2.46.0 - resolution: "@sentry/cli-win32-arm64@npm:2.46.0" +"@sentry/cli-win32-arm64@npm:2.53.0": + version: 2.53.0 + resolution: "@sentry/cli-win32-arm64@npm:2.53.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@sentry/cli-win32-i686@npm:2.46.0": - version: 2.46.0 - resolution: "@sentry/cli-win32-i686@npm:2.46.0" +"@sentry/cli-win32-i686@npm:2.53.0": + version: 2.53.0 + resolution: "@sentry/cli-win32-i686@npm:2.53.0" conditions: os=win32 & (cpu=x86 | cpu=ia32) languageName: node linkType: hard -"@sentry/cli-win32-x64@npm:2.46.0": - version: 2.46.0 - resolution: "@sentry/cli-win32-x64@npm:2.46.0" +"@sentry/cli-win32-x64@npm:2.53.0": + version: 2.53.0 + resolution: "@sentry/cli-win32-x64@npm:2.53.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@sentry/cli@npm:2.46.0": - version: 2.46.0 - resolution: "@sentry/cli@npm:2.46.0" +"@sentry/cli@npm:2.53.0": + version: 2.53.0 + resolution: "@sentry/cli@npm:2.53.0" dependencies: - "@sentry/cli-darwin": "npm:2.46.0" - "@sentry/cli-linux-arm": "npm:2.46.0" - "@sentry/cli-linux-arm64": "npm:2.46.0" - "@sentry/cli-linux-i686": "npm:2.46.0" - "@sentry/cli-linux-x64": "npm:2.46.0" - "@sentry/cli-win32-arm64": "npm:2.46.0" - "@sentry/cli-win32-i686": "npm:2.46.0" - "@sentry/cli-win32-x64": "npm:2.46.0" + "@sentry/cli-darwin": "npm:2.53.0" + "@sentry/cli-linux-arm": "npm:2.53.0" + "@sentry/cli-linux-arm64": "npm:2.53.0" + "@sentry/cli-linux-i686": "npm:2.53.0" + "@sentry/cli-linux-x64": "npm:2.53.0" + "@sentry/cli-win32-arm64": "npm:2.53.0" + "@sentry/cli-win32-i686": "npm:2.53.0" + "@sentry/cli-win32-x64": "npm:2.53.0" https-proxy-agent: "npm:^5.0.0" node-fetch: "npm:^2.6.7" progress: "npm:^2.0.3" @@ -5298,7 +5298,14 @@ __metadata: optional: true bin: sentry-cli: bin/sentry-cli - checksum: 10c0/9fd4157bf8b30afb7683bb5b436b052de77d7aa8f9fa99f438f663d4b43df18acb19a0096df5501219ffb44b5e9dbaed29826a2f49e6bc0f613e052dd4019b4b + checksum: 10c0/ea6acf44d336feb3911b07ddac4cde96a0d86cd72eb96be75ad531efa2ba713190eea02f92526b7014289dac85ede58e93575672236a8cc60695e6ad1c604df6 + languageName: node + linkType: hard + +"@sentry/core@npm:10.8.0": + version: 10.8.0 + resolution: "@sentry/core@npm:10.8.0" + checksum: 10c0/7c25eab30b9898a5feb237397576fb996477f1fbbb1f8d6720998cc6b1d6b6ec34e018db66f5bd0769aa07717d10aff8fed4e8d6cd9f0b3c95e86844df3fd405 languageName: node linkType: hard @@ -5315,13 +5322,6 @@ __metadata: languageName: node linkType: hard -"@sentry/core@npm:9.22.0": - version: 9.22.0 - resolution: "@sentry/core@npm:9.22.0" - checksum: 10c0/7c1f86778bf34bc9bf5b05d3abbcb107f266824ce293eb8ddebe0765fc12ee46c792f7ba3e7fcf626f4b44db3622413d735e3de21c3f24c3fbe48847c70896e5 - languageName: node - linkType: hard - "@sentry/core@npm:9.46.0": version: 9.46.0 resolution: "@sentry/core@npm:9.46.0" @@ -5368,16 +5368,16 @@ __metadata: languageName: node linkType: hard -"@sentry/react-native@npm:7.0.0-beta.1": - version: 7.0.0-beta.1 - resolution: "@sentry/react-native@npm:7.0.0-beta.1" +"@sentry/react-native@npm:7.0.1": + version: 7.0.1 + resolution: "@sentry/react-native@npm:7.0.1" dependencies: - "@sentry/babel-plugin-component-annotate": "npm:3.5.0" - "@sentry/browser": "npm:9.22.0" - "@sentry/cli": "npm:2.46.0" - "@sentry/core": "npm:9.22.0" - "@sentry/react": "npm:9.22.0" - "@sentry/types": "npm:9.22.0" + "@sentry/babel-plugin-component-annotate": "npm:4.3.0" + "@sentry/browser": "npm:10.8.0" + "@sentry/cli": "npm:2.53.0" + "@sentry/core": "npm:10.8.0" + "@sentry/react": "npm:10.8.0" + "@sentry/types": "npm:10.8.0" peerDependencies: expo: ">=49.0.0" react: ">=17.0.0" @@ -5387,20 +5387,20 @@ __metadata: optional: true bin: sentry-expo-upload-sourcemaps: scripts/expo-upload-sourcemaps.js - checksum: 10c0/9b6929ca40a1d8aaab02414ca088816726ee00999197272bdc5da2f57ed3beecb20596fdd842ad05ddf96b9aa3e06a75ce43efd393df2bbf836f835d7d3e0d05 + checksum: 10c0/c8d877c8a64d01777ba68ae75594140ee1795b53cbe6130bc1319bab56ce92a63404fe346231efa42e030ecf05f969a02d40eb175495553bc3c32c1853d1f8b6 languageName: node linkType: hard -"@sentry/react@npm:9.22.0": - version: 9.22.0 - resolution: "@sentry/react@npm:9.22.0" +"@sentry/react@npm:10.8.0": + version: 10.8.0 + resolution: "@sentry/react@npm:10.8.0" dependencies: - "@sentry/browser": "npm:9.22.0" - "@sentry/core": "npm:9.22.0" + "@sentry/browser": "npm:10.8.0" + "@sentry/core": "npm:10.8.0" hoist-non-react-statics: "npm:^3.3.2" peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x - checksum: 10c0/ca5451769c6be4edfdbc9a5a413e43b7e2e7ba591ed5a1dc4c19d6bdeec9c25440ac21b21ec1a4ac3eb9e1c65694c285b92d18d43ba82a7a92b808eb06d3868f + checksum: 10c0/f40c476da8e86847d93db30f488205c38a1800815b5f2bbfe694825da7a40c702b7a13fd0d4197c1e88b85bbcbcf76bf48e7ea0fd0f5c8042628d3b73843c78b languageName: node linkType: hard @@ -5430,6 +5430,15 @@ __metadata: languageName: node linkType: hard +"@sentry/types@npm:10.8.0": + version: 10.8.0 + resolution: "@sentry/types@npm:10.8.0" + dependencies: + "@sentry/core": "npm:10.8.0" + checksum: 10c0/55c728072b94cf82404c300fd317fbef039a0d766be019d33476fb78348032cf07d3effca3a19d07f7db2a47c4d8f815fd2cc711cab2ab310e24102dcf72a675 + languageName: node + linkType: hard + "@sentry/types@npm:5.30.0": version: 5.30.0 resolution: "@sentry/types@npm:5.30.0" @@ -5437,15 +5446,6 @@ __metadata: languageName: node linkType: hard -"@sentry/types@npm:9.22.0": - version: 9.22.0 - resolution: "@sentry/types@npm:9.22.0" - dependencies: - "@sentry/core": "npm:9.22.0" - checksum: 10c0/cd806692c95e4f12ca4c043e969b4a1f5ca5fa92a2c116e2d99617f77141c729e0c49ecfe341b73d1959eb9ae7899d093f1cf583b392d64758ab8e486d2f2d30 - languageName: node - linkType: hard - "@sentry/utils@npm:5.30.0": version: 5.30.0 resolution: "@sentry/utils@npm:5.30.0" From 77626c423ffef9f1130ecd9a05be1a7b10bf6513 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Sat, 13 Sep 2025 17:45:40 -0700 Subject: [PATCH 17/25] chore: update tooling dependencies (#1069) * chore: update tooling dependencies * chore: align react typings and node types * update lock --- .gitignore | 1 + package-lock.json | 233 ---------------------------------------------- package.json | 18 ++-- yarn.lock | 87 ++++------------- 4 files changed, 28 insertions(+), 311 deletions(-) delete mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index 3bea386a9..58c107f3b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ showcase output/* *.tsbuildinfo .yarnrc.yml +package-lock.json # CI-generated tarballs (don't commit these!) mobile-sdk-alpha-ci.tgz diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index ddef32ffd..000000000 --- a/package-lock.json +++ /dev/null @@ -1,233 +0,0 @@ -{ - "name": "self", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "devDependencies": { - "ts-node": "^10.9.2", - "typescript": "^5.8.3" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.0.tgz", - "integrity": "sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "undici-types": "~7.8.0" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - } - } -} diff --git a/package.json b/package.json index 8e4532693..43312863f 100644 --- a/package.json +++ b/package.json @@ -29,20 +29,24 @@ "types": "yarn workspaces foreach --topological-dev --parallel --exclude @selfxyz/contracts -i --all run types " }, "resolutions": { - "@babel/core": "^7.28.3", - "@babel/runtime": "^7.28.3", - "@typescript-eslint/eslint-plugin": "^8.39.0", - "@typescript-eslint/parser": "^8.39.0", + "@babel/core": "^7.28.4", + "@babel/runtime": "^7.28.4", + "@typescript-eslint/eslint-plugin": "^8.43.0", + "@typescript-eslint/parser": "^8.43.0", + "@types/node": "^22.18.3", + "@types/react": "^18.3.4", + "@types/react-dom": "^18.3.0", "react": "^18.3.1", + "react-dom": "^18.3.1", "react-native": "0.76.9" }, "devDependencies": { - "@types/node": "^22.0.0", + "@types/node": "^22.18.3", "gitleaks": "1.0.0", "husky": "9.1.7", - "knip": "^5.62.0", + "knip": "^5.63.1", "patch-package": "^8.0.0", - "prettier": "^3.5.3", + "prettier": "^3.6.2", "typescript": "^5.9.2" }, "packageManager": "yarn@4.6.0", diff --git a/yarn.lock b/yarn.lock index f4da13f7a..76f95302e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -107,7 +107,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.28.3": +"@babel/core@npm:^7.28.4": version: 7.28.4 resolution: "@babel/core@npm:7.28.4" dependencies: @@ -1157,7 +1157,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.28.3": +"@babel/runtime@npm:^7.28.4": version: 7.28.4 resolution: "@babel/runtime@npm:7.28.4" checksum: 10c0/792ce7af9750fb9b93879cc9d1db175701c4689da890e6ced242ea0207c9da411ccf16dc04e689cc01158b28d7898c40d75598f4559109f761c12ce01e959bf7 @@ -9650,44 +9650,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*": - version: 24.3.1 - resolution: "@types/node@npm:24.3.1" - dependencies: - undici-types: "npm:~7.10.0" - checksum: 10c0/99b86fc32294fcd61136ca1f771026443a1e370e9f284f75e243b29299dd878e18c193deba1ce29a374932db4e30eb80826e1049b9aad02d36f5c30b94b6f928 - languageName: node - linkType: hard - -"@types/node@npm:22.7.5": - version: 22.7.5 - resolution: "@types/node@npm:22.7.5" - dependencies: - undici-types: "npm:~6.19.2" - checksum: 10c0/cf11f74f1a26053ec58066616e3a8685b6bcd7259bc569738b8f752009f9f0f7f85a1b2d24908e5b0f752482d1e8b6babdf1fbb25758711ec7bb9500bfcd6e60 - languageName: node - linkType: hard - -"@types/node@npm:^10.0.3": - version: 10.17.60 - resolution: "@types/node@npm:10.17.60" - checksum: 10c0/0742294912a6e79786cdee9ed77cff6ee8ff007b55d8e21170fc3e5994ad3a8101fea741898091876f8dc32b0a5ae3d64537b7176799e92da56346028d2cbcd2 - languageName: node - linkType: hard - -"@types/node@npm:^22.0.0": - version: 22.18.1 - resolution: "@types/node@npm:22.18.1" +"@types/node@npm:^22.18.3": + version: 22.18.3 + resolution: "@types/node@npm:22.18.3" dependencies: undici-types: "npm:~6.21.0" - checksum: 10c0/1912b0ea6cb9ef59722b0fed64652388e13b41d52569c16198f1278a882837bbf4c8a4ec913e852893356f07c0c44b4e00fbca289ac7222741d03449104e22fe - languageName: node - linkType: hard - -"@types/node@npm:^8.0.0": - version: 8.10.66 - resolution: "@types/node@npm:8.10.66" - checksum: 10c0/425e0fca5bad0d6ff14336946a1e3577750dcfbb7449614786d3241ca78ff44e3beb43eace122682de1b9d8e25cf2a0456a0b3e500d78cb55cab68f892e38141 + checksum: 10c0/29ec5674a7eaac50c4c779e937c4449e813ff70c411eb525b6e12837e7b88c39219c3c2f29e39ea8f72cd6b7975f8696dc05f78c07bce35a241e0c9eaf5523a8 languageName: node linkType: hard @@ -9728,7 +9696,7 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^18.0.0, @types/react-dom@npm:^18.3.0": +"@types/react-dom@npm:^18.3.0": version: 18.3.7 resolution: "@types/react-dom@npm:18.3.7" peerDependencies: @@ -9770,16 +9738,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*": - version: 19.1.12 - resolution: "@types/react@npm:19.1.12" - dependencies: - csstype: "npm:^3.0.2" - checksum: 10c0/e35912b43da0caaab5252444bab87a31ca22950cde2822b3b3dc32e39c2d42dad1a4cf7b5dde9783aa2d007f0b2cba6ab9563fc6d2dbcaaa833b35178118767c - languageName: node - linkType: hard - -"@types/react@npm:^18, @types/react@npm:^18.3.4": +"@types/react@npm:^18.3.4": version: 18.3.24 resolution: "@types/react@npm:18.3.24" dependencies: @@ -9890,7 +9849,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^8.39.0": +"@typescript-eslint/eslint-plugin@npm:^8.43.0": version: 8.43.0 resolution: "@typescript-eslint/eslint-plugin@npm:8.43.0" dependencies: @@ -9911,7 +9870,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:^8.39.0": +"@typescript-eslint/parser@npm:^8.43.0": version: 8.43.0 resolution: "@typescript-eslint/parser@npm:8.43.0" dependencies: @@ -18870,7 +18829,7 @@ __metadata: languageName: node linkType: hard -"knip@npm:^5.62.0": +"knip@npm:^5.63.1": version: 5.63.1 resolution: "knip@npm:5.63.1" dependencies: @@ -21531,7 +21490,7 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^3.5.3": +"prettier@npm:^3.5.3, prettier@npm:^3.6.2": version: 3.6.2 resolution: "prettier@npm:3.6.2" bin: @@ -21934,7 +21893,7 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^18.2.0, react-dom@npm:^18.3.1": +"react-dom@npm:^18.3.1": version: 18.3.1 resolution: "react-dom@npm:18.3.1" dependencies: @@ -23229,12 +23188,12 @@ __metadata: version: 0.0.0-use.local resolution: "self-workspace-root@workspace:." dependencies: - "@types/node": "npm:^22.0.0" + "@types/node": "npm:^22.18.3" gitleaks: "npm:1.0.0" husky: "npm:9.1.7" - knip: "npm:^5.62.0" + knip: "npm:^5.63.1" patch-package: "npm:^8.0.0" - prettier: "npm:^3.5.3" + prettier: "npm:^3.6.2" typescript: "npm:^5.9.2" languageName: unknown linkType: soft @@ -25565,13 +25524,6 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.19.2": - version: 6.19.8 - resolution: "undici-types@npm:6.19.8" - checksum: 10c0/078afa5990fba110f6824823ace86073b4638f1d5112ee26e790155f481f2a868cc3e0615505b6f4282bdf74a3d8caad715fd809e870c2bb0704e3ea6082f344 - languageName: node - linkType: hard - "undici-types@npm:~6.21.0": version: 6.21.0 resolution: "undici-types@npm:6.21.0" @@ -25579,13 +25531,6 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~7.10.0": - version: 7.10.0 - resolution: "undici-types@npm:7.10.0" - checksum: 10c0/8b00ce50e235fe3cc601307f148b5e8fb427092ee3b23e8118ec0a5d7f68eca8cee468c8fc9f15cbb2cf2a3797945ebceb1cbd9732306a1d00e0a9b6afa0f635 - languageName: node - linkType: hard - "undici@npm:^5.14.0": version: 5.29.0 resolution: "undici@npm:5.29.0" From 9ccce41f304b2800b99a1ecd4d1c1387ee1ff376 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Sat, 13 Sep 2025 17:55:35 -0700 Subject: [PATCH 18/25] chore: minor fixes across monorepo (#1068) * small fixes * fixes * fix gesture handler error * ci fixes --- .github/actions/mobile-setup/action.yml | 6 +- .github/workflows/mobile-bundle-analysis.yml | 4 +- .github/workflows/mobile-ci.yml | 4 + .github/workflows/mobile-e2e.yml | 4 +- app/package.json | 11 +- app/src/screens/dev/CreateMockScreen.tsx | 6 +- .../document/DocumentNFCScanScreen.tsx | 29 +++-- .../document/DocumentNFCTroubleScreen.tsx | 9 +- app/src/screens/settings/SettingsScreen.tsx | 120 +++++++++--------- .../mobile-sdk-alpha/react-native.config.cjs | 3 +- 10 files changed, 105 insertions(+), 91 deletions(-) diff --git a/.github/actions/mobile-setup/action.yml b/.github/actions/mobile-setup/action.yml index c06757f13..c62086b27 100644 --- a/.github/actions/mobile-setup/action.yml +++ b/.github/actions/mobile-setup/action.yml @@ -78,11 +78,7 @@ runs: fi # Run mobile-specific installation - if [[ "${{ runner.os }}" == "macOS" ]]; then - yarn install-app:mobile-deploy:ios - else - yarn install-app:mobile-deploy - fi + yarn install-app:mobile-deploy # Install Ruby gems with bundler (respecting cache) echo "📦 Installing Ruby gems with strict lock file..." diff --git a/.github/workflows/mobile-bundle-analysis.yml b/.github/workflows/mobile-bundle-analysis.yml index bc29bbf61..cff87db95 100644 --- a/.github/workflows/mobile-bundle-analysis.yml +++ b/.github/workflows/mobile-bundle-analysis.yml @@ -17,7 +17,7 @@ on: jobs: analyze-android: - runs-on: macos-14 + runs-on: macos-latest-large steps: - uses: actions/checkout@v4 - name: Read and sanitize Node.js version @@ -80,7 +80,7 @@ jobs: working-directory: ./app analyze-ios: - runs-on: macos-14 + runs-on: macos-latest-large steps: - uses: actions/checkout@v4 - name: Read and sanitize Node.js version diff --git a/.github/workflows/mobile-ci.yml b/.github/workflows/mobile-ci.yml index eb16210a3..db3e81a52 100644 --- a/.github/workflows/mobile-ci.yml +++ b/.github/workflows/mobile-ci.yml @@ -36,6 +36,7 @@ concurrency: jobs: build-deps: runs-on: macos-latest-large + timeout-minutes: 60 steps: - uses: actions/checkout@v4 - name: Read and sanitize Node.js version @@ -92,6 +93,7 @@ jobs: test: runs-on: macos-latest-large needs: build-deps + timeout-minutes: 60 steps: - uses: actions/checkout@v4 - name: Read and sanitize Node.js version @@ -193,6 +195,7 @@ jobs: build-ios: runs-on: macos-latest-large needs: build-deps + timeout-minutes: 60 env: # iOS project configuration - hardcoded for CI stability IOS_PROJECT_NAME: "Self" @@ -381,6 +384,7 @@ jobs: build-android: runs-on: ubuntu-latest needs: build-deps + timeout-minutes: 60 steps: - uses: actions/checkout@v4 - name: Read and sanitize Node.js version diff --git a/.github/workflows/mobile-e2e.yml b/.github/workflows/mobile-e2e.yml index 3fd909c0b..3bd1dae0d 100644 --- a/.github/workflows/mobile-e2e.yml +++ b/.github/workflows/mobile-e2e.yml @@ -32,7 +32,7 @@ jobs: concurrency: group: ${{ github.workflow }}-android-${{ github.ref }} cancel-in-progress: true - timeout-minutes: 45 + timeout-minutes: 60 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -135,7 +135,7 @@ jobs: if-no-files-found: warn e2e-ios: - timeout-minutes: 45 + timeout-minutes: 60 runs-on: macos-latest-large concurrency: group: ${{ github.workflow }}-ios-${{ github.ref }} diff --git a/app/package.json b/app/package.json index 3b40248f7..52e4b99b9 100644 --- a/app/package.json +++ b/app/package.json @@ -14,8 +14,7 @@ "bump-version:major": "npm version major && yarn sync-versions", "bump-version:minor": "npm version minor && yarn sync-versions", "bump-version:patch": "npm version patch && yarn sync-versions", - "clean": "yarn clean:watchman && yarn clean:build && yarn clean:ios && yarn clean:android && yarn clean:xcode && yarn clean:pod-cache && yarn clean:node", - "clean:android": "rm -rf android/app/build android/build", + "clean": "yarn clean:watchman && yarn clean:build && yarn clean:ios && yarn clean:xcode && yarn clean:pod-cache && yarn clean:node", "clean:build": "rm -rf ios/build android/app/build android/build", "clean:ios": "rm -rf ios/Pods ios/Podfile.lock Gemfile.lock", "clean:node": "rm -rf ../node_modules app/node_modules", @@ -31,7 +30,6 @@ "imports:fix": "node ./scripts/alias-imports.cjs", "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:mobile-deploy:ios": "yarn install-app:setup && yarn clean:xcode-env-local", "install-app:setup": "yarn install && yarn build:deps && cd ios && bundle install && scripts/pod-install-with-cache-fix.sh && cd ..", "ios": "yarn build:deps && react-native run-ios --scheme OpenPassport", "ios:fastlane-debug": "yarn reinstall && bundle exec fastlane --verbose ios internal_test", @@ -51,6 +49,7 @@ "release:patch": "./scripts/release.sh patch", "setup": "yarn clean:build && yarn install && yarn build:deps && cd ios && bundle install && bundle exec pod install --repo-update && cd .. && yarn clean:xcode-env-local", "start": "watchman watch-del-all && react-native start", + "start:clean": "watchman watch-del-all && cd android && ./gradlew clean && cd .. && react-native start --reset-cache", "sync-versions": "bundle exec fastlane ios sync_version && bundle exec fastlane android sync_version", "tag:release": "node scripts/tag.cjs release", "tag:remove": "node scripts/tag.cjs remove", @@ -200,5 +199,11 @@ "packageManager": "yarn@4.6.0", "engines": { "node": ">=22 <23" + }, + "resolutions": { + "punycode": "npm:punycode.js@^2.3.1" + }, + "overrides": { + "punycode": "npm:punycode.js@^2.3.1" } } diff --git a/app/src/screens/dev/CreateMockScreen.tsx b/app/src/screens/dev/CreateMockScreen.tsx index 4febc76fd..7f766c1ab 100644 --- a/app/src/screens/dev/CreateMockScreen.tsx +++ b/app/src/screens/dev/CreateMockScreen.tsx @@ -5,7 +5,7 @@ import { flag } from 'country-emoji'; import getCountryISO2 from 'country-iso-3-to-2'; import React, { useCallback, useState } from 'react'; -import { TouchableOpacity } from 'react-native'; +import { TouchableOpacity, View } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { @@ -223,7 +223,9 @@ const CreateMockScreen: React.FC = () => { > - + + + diff --git a/app/src/screens/document/DocumentNFCScanScreen.tsx b/app/src/screens/document/DocumentNFCScanScreen.tsx index abbfe683e..faaf6a459 100644 --- a/app/src/screens/document/DocumentNFCScanScreen.tsx +++ b/app/src/screens/document/DocumentNFCScanScreen.tsx @@ -10,6 +10,7 @@ import { NativeModules, Platform, StyleSheet, + View, } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import NfcManager from 'react-native-nfc-manager'; @@ -627,19 +628,21 @@ const DocumentNFCScanScreen: React.FC = () => { <> - - Verify your ID - - - {social.map(([Icon, href], i) => ( - - ))} - - - SELF - - {/* Dont remove if not viewing on ios */} - + + + {social.map(([Icon, href], i) => ( + + ))} + + + SELF + + {/* Dont remove if not viewing on ios */} + + - - + + ); }; diff --git a/packages/mobile-sdk-alpha/react-native.config.cjs b/packages/mobile-sdk-alpha/react-native.config.cjs index ea4621bc7..5e9eee27a 100644 --- a/packages/mobile-sdk-alpha/react-native.config.cjs +++ b/packages/mobile-sdk-alpha/react-native.config.cjs @@ -6,13 +6,12 @@ const path = require('path'); const fs = require('fs'); const packageRoot = fs.realpathSync(__dirname); -const iosSourceDir = fs.realpathSync(path.join(packageRoot, 'ios')); +const _iosSourceDir = fs.realpathSync(path.join(packageRoot, 'ios')); module.exports = { dependency: { platforms: { ios: { - sourceDir: iosSourceDir, podspecPath: path.join(packageRoot, 'mobile-sdk-alpha.podspec'), }, android: null, From 4e97eec42e8e422c672c5b25f072b116ce824d61 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 16 Sep 2025 00:32:38 -0700 Subject: [PATCH 19/25] fix yarn build; add workflow ci (#1075) * add new workspace ci * disable package version check for now * build before checks * format * fix in future pr --- .github/workflows/workspace-ci.yml | 198 +++++++++++++++++++++++++++++ circuits/tsconfig.json | 2 +- circuits/tsconfig.types.json | 12 +- scripts/check-package-versions.mjs | 54 +++++++- 4 files changed, 255 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/workspace-ci.yml diff --git a/.github/workflows/workspace-ci.yml b/.github/workflows/workspace-ci.yml new file mode 100644 index 000000000..f81c44c9a --- /dev/null +++ b/.github/workflows/workspace-ci.yml @@ -0,0 +1,198 @@ +name: Workspace CI + +on: + pull_request: + branches: + - dev + - staging + - main + push: + branches: + - dev + - staging + - main + +jobs: + workspace-build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Cache Yarn dependencies + uses: ./.github/actions/cache-yarn + with: + path: | + .yarn/cache + node_modules + */node_modules + packages/*/node_modules + cache-version: v1 + + - name: Install Dependencies + uses: ./.github/actions/yarn-install + + - name: Run workspace build + run: yarn build + + - name: Verify build artifacts + run: | + echo "Checking for build artifacts..." + find . -name "dist" -type d | head -10 + echo "Build completed successfully!" + + workspace-type-check: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Cache Yarn dependencies + uses: ./.github/actions/cache-yarn + with: + path: | + .yarn/cache + node_modules + */node_modules + packages/*/node_modules + cache-version: v1 + + - name: Install Dependencies + uses: ./.github/actions/yarn-install + + - name: Build workspace dependencies + run: yarn build + + - name: Run workspace type checking + run: yarn types + + - name: Verify type checking passed + run: echo "Type checking completed successfully!" + + workspace-lint: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Cache Yarn dependencies + uses: ./.github/actions/cache-yarn + with: + path: | + .yarn/cache + node_modules + */node_modules + packages/*/node_modules + cache-version: v1 + + - name: Install Dependencies + uses: ./.github/actions/yarn-install + + - name: Build workspace dependencies + run: yarn build + + - name: Run workspace linting + run: yarn lint + + - name: Verify linting passed + run: echo "Linting completed successfully!" + + workspace-format-check: + if: false + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Cache Yarn dependencies + uses: ./.github/actions/cache-yarn + with: + path: | + .yarn/cache + node_modules + */node_modules + packages/*/node_modules + cache-version: v1 + + - name: Install Dependencies + uses: ./.github/actions/yarn-install + + - name: Check code formatting (dry run) + run: | + echo "Checking if code is properly formatted..." + # Run format in dry-run mode by checking if any files would change + if ! git diff --quiet --exit-code; then + echo "Working directory not clean before format check" + git status --porcelain + exit 1 + fi + yarn format + if ! git diff --quiet --exit-code; then + echo "❌ Code formatting issues found. The following files need formatting:" + git diff --name-only + echo "Run 'yarn format' to fix these issues." + exit 1 + fi + + - name: Verify formatting check passed + run: echo "Code formatting check completed successfully!" + + workspace-test: + if: false + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Cache Yarn dependencies + uses: ./.github/actions/cache-yarn + with: + path: | + .yarn/cache + node_modules + */node_modules + packages/*/node_modules + cache-version: v1 + + - name: Install Dependencies + uses: ./.github/actions/yarn-install + + - name: Build workspace dependencies + run: yarn build + + - name: Run workspace tests + run: yarn test + + - name: Verify tests passed + run: echo "Workspace tests completed successfully!" + + # version-consistency: + # runs-on: ubuntu-latest + # permissions: + # contents: read + # steps: + # - uses: actions/checkout@v4 + + # - name: Cache Yarn dependencies + # uses: ./.github/actions/cache-yarn + # with: + # path: | + # .yarn/cache + # node_modules + # */node_modules + # packages/*/node_modules + # cache-version: v1 + + # - name: Install Dependencies + # uses: ./.github/actions/yarn-install + + # - name: Check package version consistency + # run: yarn check:versions + + # - name: Verify version consistency + # run: echo "Package version consistency check passed!" diff --git a/circuits/tsconfig.json b/circuits/tsconfig.json index a7991e536..bd45869f2 100644 --- a/circuits/tsconfig.json +++ b/circuits/tsconfig.json @@ -13,7 +13,7 @@ "baseUrl": ".", "composite": false }, - "include": ["tests/**/*", "tests/**/*.json", "src/**/*"], + "include": ["tests/**/*", "tests/**/*.json", "src/**/*", "../common/src/**/*"], "exclude": ["node_modules"], "references": [ { diff --git a/circuits/tsconfig.types.json b/circuits/tsconfig.types.json index 9638cd723..f618f1ef3 100644 --- a/circuits/tsconfig.types.json +++ b/circuits/tsconfig.types.json @@ -3,13 +3,9 @@ "compilerOptions": { "declaration": true, "emitDeclarationOnly": true, - "declarationMap": false, - "outDir": "./dist/esm", - "declarationDir": "./dist/esm", - "composite": true, - "noEmit": false, - "rootDir": "." + "outDir": "./dist/types", + "skipLibCheck": true }, - "include": ["src/**/*", "tests/**/*", "tests/**/*.json"], - "exclude": ["node_modules", "dist"] + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] } diff --git a/scripts/check-package-versions.mjs b/scripts/check-package-versions.mjs index bbc1a6252..29aacdc68 100755 --- a/scripts/check-package-versions.mjs +++ b/scripts/check-package-versions.mjs @@ -276,7 +276,17 @@ if (engineNodeVersions && engineNodeVersions.size > 0) { const workflowNodeVersions = workflowVersions.get('workflow node-version'); if (workflowNodeVersions) { const mismatches = [...workflowNodeVersions.keys()] - .filter(v => !String(v).includes(expectedNodeVersion)) + .filter(v => { + const versionStr = String(v); + // Skip dynamic versions like ${{ env.NODE_VERSION }} - these are set from .nvmrc + if ( + versionStr.includes('${{') || + versionStr.includes('env.NODE_VERSION') + ) { + return false; + } + return !versionStr.includes(expectedNodeVersion); + }) .sort(); if (mismatches.length) { console.log('🚨 WORKFLOW VERSION MISMATCH:'); @@ -420,6 +430,12 @@ if (totalIssues === 0) { for (const category of categories) { const mismatchedInCategory = category.packages.filter(pkg => { + if ( + criticalPackages.includes(pkg) || + intentionallyDifferentPackages.includes(pkg) + ) { + return false; // Skip already reported packages + } const versions = depVersions.get(pkg); return versions && versions.size > 1; }); @@ -436,4 +452,38 @@ if (totalIssues === 0) { } } -process.exit(totalIssues > 0 ? 1 : 0); +// Only fail CI for critical issues that can break builds or security +const criticalIssues = [ + hasCriticalIssues, + hasWorkflowIssues, + hasPmIssues, +].filter(Boolean).length; + +if (criticalIssues > 0) { + console.log( + `\n🚨 FAILING CI: Found ${criticalIssues} critical issue(s) that must be fixed.`, + ); + process.exit(1); +} else if (hasOtherIssues || hasIntentionalDifferences) { + let message = '⚠️ CI PASSING: '; + const parts = []; + if (hasOtherIssues) parts.push('non-critical version mismatches'); + if (hasIntentionalDifferences) + parts.push('intentional technical differences'); + message += `Found ${parts.join(' and ')}.`; + + console.log(`\n${message}`); + if (hasOtherIssues) { + console.log( + 'Non-critical mismatches should be addressed but do not block development.', + ); + } + if (hasIntentionalDifferences) { + console.log( + 'Intentional differences are acceptable for technical requirements.', + ); + } + process.exit(0); +} else { + process.exit(0); +} From a2c696915828fa941c5f6943881dba715286cbc9 Mon Sep 17 00:00:00 2001 From: Nesopie <87437291+Nesopie@users.noreply.github.com> Date: Wed, 17 Sep 2025 01:58:33 +0530 Subject: [PATCH 20/25] feat: add functions for disclosing aadhaar attributes (#1033) * feat: add functions for disclosing aadhaar attributes * format --- common/index.ts | 15 +- common/src/utils/aadhaar/mockData.ts | 402 ++++++++++++++++++--------- 2 files changed, 270 insertions(+), 147 deletions(-) diff --git a/common/index.ts b/common/index.ts index 15ef480ca..2bf8677f7 100644 --- a/common/index.ts +++ b/common/index.ts @@ -84,6 +84,13 @@ export { stringToBigInt, } from './src/utils/index.js'; +export { + prepareAadhaarRegisterTestData, + prepareAadhaarDiscloseTestData, + prepareAadhaarRegisterData, + prepareAadhaarDiscloseData, +} from './src/utils/aadhaar/mockData.js'; +export { generateTestData, testCustomData } from './src/utils/aadhaar/utils.js'; export { createSelector } from './src/utils/aadhaar/constants.js'; // Hash utilities export { @@ -93,11 +100,3 @@ export { hash, packBytesAndPoseidon, } from './src/utils/hash.js'; - -export { generateTestData, testCustomData } from './src/utils/aadhaar/utils.js'; - -export { - prepareAadhaarDiscloseTestData, - prepareAadhaarRegisterData, - prepareAadhaarRegisterTestData, -} from './src/utils/aadhaar/mockData.js'; diff --git a/common/src/utils/aadhaar/mockData.ts b/common/src/utils/aadhaar/mockData.ts index 5ba5eff5e..e6caa3e19 100644 --- a/common/src/utils/aadhaar/mockData.ts +++ b/common/src/utils/aadhaar/mockData.ts @@ -1,6 +1,5 @@ import forge from 'node-forge'; import { poseidon5 } from 'poseidon-lite'; - import { COMMITMENT_TREE_DEPTH } from '../../constants/constants.js'; import { findIndexInTree, formatInput } from '../circuits/generateInputs.js'; import { packBytesAndPoseidon } from '../hash.js'; @@ -11,24 +10,20 @@ import { getNameYobLeafAahaar, } from '../trees.js'; import { testQRData } from './assets/dataInput.js'; -import { - calculateAge, - extractQRDataFields, - generateTestData, - stringToAsciiArray, - testCustomData, -} from './utils.js'; - +import { calculateAge, generateTestData, stringToAsciiArray, testCustomData } from './utils.js'; +import { extractQRDataFields } from './utils.js'; +import { AadhaarField, createSelector } from './constants.js'; +import { formatCountriesList } from '../circuits/formatInputs.js'; +import { LeanIMT } from '@openpassport/zk-kit-lean-imt'; +import { SMT } from '@openpassport/zk-kit-smt'; import { convertBigIntToByteArray, decompressByteArray, extractPhoto, splitToWords, } from '@anon-aadhaar/core'; -import { LeanIMT } from '@openpassport/zk-kit-lean-imt'; -import { SMT } from '@openpassport/zk-kit-smt'; -import { bufferToHex, Uint8ArrayToCharArray } from '@zk-email/helpers/dist/binary-format.js'; import { sha256Pad } from '@zk-email/helpers/dist/sha-utils.js'; +import { bufferToHex, Uint8ArrayToCharArray } from '@zk-email/helpers/dist/binary-format.js'; // Helper function to compute padded name function computePaddedName(name: string): number[] { @@ -168,25 +163,19 @@ function processQRDataSimple(qrData: string) { }; } -export function prepareAadhaarDiscloseTestData( - privateKeyPem: string, - merkletree: LeanIMT, - nameAndDob_smt: SMT, - nameAndYob_smt: SMT, - scope: string, +export function prepareAadhaarRegisterTestData( + privKeyPem: string, + pubkeyPem: string, secret: string, - user_identifier: string, - selector: string, name?: string, dateOfBirth?: string, gender?: string, pincode?: string, state?: string, - timestamp?: string, - updateTree?: boolean + timestamp?: string ) { const sharedData = processQRData( - privateKeyPem, + privKeyPem, name, dateOfBirth, gender, @@ -195,14 +184,36 @@ export function prepareAadhaarDiscloseTestData( timestamp ); - const { age, currentYear, currentMonth, currentDay } = calculateAge( - sharedData.extractedFields.dob, - sharedData.extractedFields.mob, - sharedData.extractedFields.yob + const delimiterIndices: number[] = []; + for (let i = 0; i < sharedData.qrDataPadded.length; i++) { + if (sharedData.qrDataPadded[i] === 255) { + delimiterIndices.push(i); + } + if (delimiterIndices.length === 18) { + break; + } + } + let photoEOI = 0; + for (let i = delimiterIndices[17]; i < sharedData.qrDataPadded.length - 1; i++) { + if (sharedData.qrDataPadded[i + 1] === 217 && sharedData.qrDataPadded[i] === 255) { + photoEOI = i + 1; + } + } + if (photoEOI === 0) { + throw new Error('Photo EOI not found'); + } + + const signatureBytes = sharedData.decodedData.slice( + sharedData.decodedData.length - 256, + sharedData.decodedData.length ); + const signature = BigInt('0x' + bufferToHex(Buffer.from(signatureBytes)).toString()); + + const publicKey = forge.pki.publicKeyFromPem(pubkeyPem); + + const modulusHex = publicKey.n.toString(16); + const pubKey = BigInt('0x' + modulusHex); - const uppercaseName = computeUppercasePaddedName(sharedData.extractedFields.name); - const genderAscii = stringToAsciiArray(sharedData.extractedFields.gender)[0]; const nullifier = nullifierHash(sharedData.extractedFields); const packedCommitment = computePackedCommitment(sharedData.extractedFields); const commitment = computeCommitment( @@ -213,74 +224,14 @@ export function prepareAadhaarDiscloseTestData( BigInt(sharedData.photoHash) ); - const paddedName = computePaddedName(sharedData.extractedFields.name); - - if (updateTree) { - merkletree.insert(BigInt(commitment)); - } - - const index = findIndexInTree(merkletree, BigInt(commitment)); - const { - siblings, - path: merkle_path, - leaf_depth, - } = generateMerkleProof(merkletree, index, COMMITMENT_TREE_DEPTH); - - const namedob_leaf = getNameDobLeafAadhaar( - sharedData.extractedFields.name, - sharedData.extractedFields.yob, - sharedData.extractedFields.mob, - sharedData.extractedFields.dob - ); - const nameyob_leaf = getNameYobLeafAahaar( - sharedData.extractedFields.name, - sharedData.extractedFields.yob - ); - - const { - root: ofac_name_dob_smt_root, - closestleaf: ofac_name_dob_smt_leaf_key, - siblings: ofac_name_dob_smt_siblings, - } = generateSMTProof(nameAndDob_smt, namedob_leaf); - - const { - root: ofac_name_yob_smt_root, - closestleaf: ofac_name_yob_smt_leaf_key, - siblings: ofac_name_yob_smt_siblings, - } = generateSMTProof(nameAndYob_smt, nameyob_leaf); - const inputs = { - attestation_id: '3', + qrDataPadded: Uint8ArrayToCharArray(sharedData.qrDataPadded), + qrDataPaddedLength: sharedData.qrDataPaddedLen, + delimiterIndices: delimiterIndices, + signature: splitToWords(signature, BigInt(121), BigInt(17)), + pubKey: splitToWords(pubKey, BigInt(121), BigInt(17)), secret: secret, - qrDataHash: BigInt(sharedData.qrHash).toString(), - gender: genderAscii.toString(), - yob: stringToAsciiArray(sharedData.extractedFields.yob), - mob: stringToAsciiArray(sharedData.extractedFields.mob), - dob: stringToAsciiArray(sharedData.extractedFields.dob), - name: formatInput(paddedName), - aadhaar_last_4digits: stringToAsciiArray(sharedData.extractedFields.aadhaarLast4Digits), - pincode: stringToAsciiArray(sharedData.extractedFields.pincode), - state: stringToAsciiArray(sharedData.extractedFields.state.padEnd(31, '\0')), - ph_no_last_4digits: stringToAsciiArray(sharedData.extractedFields.phoneNoLast4Digits), - photoHash: formatInput(BigInt(sharedData.photoHash)), - merkle_root: formatInput(BigInt(merkletree.root)), - leaf_depth: formatInput(leaf_depth), - path: formatInput(merkle_path), - siblings: formatInput(siblings), - ofac_name_dob_smt_leaf_key: formatInput(BigInt(ofac_name_dob_smt_leaf_key)), - ofac_name_dob_smt_root: formatInput(BigInt(ofac_name_dob_smt_root)), - ofac_name_dob_smt_siblings: formatInput(ofac_name_dob_smt_siblings), - ofac_name_yob_smt_leaf_key: formatInput(BigInt(ofac_name_yob_smt_leaf_key)), - ofac_name_yob_smt_root: formatInput(BigInt(ofac_name_yob_smt_root)), - ofac_name_yob_smt_siblings: formatInput(ofac_name_yob_smt_siblings), - selector, - minimumAge: formatInput(age - 2), - currentYear: formatInput(currentYear), - currentMonth: formatInput(currentMonth), - currentDay: formatInput(currentDay), - scope: formatInput(BigInt(scope)), - user_identifier: formatInput(BigInt(user_identifier)), - forbidden_countries_list: [...Array(120)].map((x) => '0'), + photoEOI: photoEOI, }; return { @@ -365,26 +316,28 @@ export async function prepareAadhaarRegisterData(qrData: string, secret: string, photoEOI: photoEOI, }; - return { - inputs, - nullifier, - commitment, - }; + return inputs; } -export function prepareAadhaarRegisterTestData( - privKeyPem: string, - pubkeyPem: string, +export function prepareAadhaarDiscloseTestData( + privateKeyPem: string, + merkletree: LeanIMT, + nameAndDob_smt: SMT, + nameAndYob_smt: SMT, + scope: string, secret: string, + user_identifier: string, + selector: string, name?: string, dateOfBirth?: string, gender?: string, pincode?: string, state?: string, - timestamp?: string + timestamp?: string, + updateTree?: boolean ) { const sharedData = processQRData( - privKeyPem, + privateKeyPem, name, dateOfBirth, gender, @@ -393,36 +346,13 @@ export function prepareAadhaarRegisterTestData( timestamp ); - const delimiterIndices: number[] = []; - for (let i = 0; i < sharedData.qrDataPadded.length; i++) { - if (sharedData.qrDataPadded[i] === 255) { - delimiterIndices.push(i); - } - if (delimiterIndices.length === 18) { - break; - } - } - let photoEOI = 0; - for (let i = delimiterIndices[17]; i < sharedData.qrDataPadded.length - 1; i++) { - if (sharedData.qrDataPadded[i + 1] === 217 && sharedData.qrDataPadded[i] === 255) { - photoEOI = i + 1; - } - } - if (photoEOI === 0) { - throw new Error('Photo EOI not found'); - } - - const signatureBytes = sharedData.decodedData.slice( - sharedData.decodedData.length - 256, - sharedData.decodedData.length + const { age, currentYear, currentMonth, currentDay } = calculateAge( + sharedData.extractedFields.dob, + sharedData.extractedFields.mob, + sharedData.extractedFields.yob ); - const signature = BigInt('0x' + bufferToHex(Buffer.from(signatureBytes)).toString()); - - const publicKey = forge.pki.publicKeyFromPem(pubkeyPem); - - const modulusHex = publicKey.n.toString(16); - const pubKey = BigInt('0x' + modulusHex); + const genderAscii = stringToAsciiArray(sharedData.extractedFields.gender)[0]; const nullifier = nullifierHash(sharedData.extractedFields); const packedCommitment = computePackedCommitment(sharedData.extractedFields); const commitment = computeCommitment( @@ -433,14 +363,74 @@ export function prepareAadhaarRegisterTestData( BigInt(sharedData.photoHash) ); + const paddedName = computePaddedName(sharedData.extractedFields.name); + + if (updateTree) { + merkletree.insert(BigInt(commitment)); + } + + const index = findIndexInTree(merkletree, BigInt(commitment)); + const { + siblings, + path: merkle_path, + leaf_depth, + } = generateMerkleProof(merkletree, index, COMMITMENT_TREE_DEPTH); + + const namedob_leaf = getNameDobLeafAadhaar( + sharedData.extractedFields.name, + sharedData.extractedFields.yob, + sharedData.extractedFields.mob, + sharedData.extractedFields.dob + ); + const nameyob_leaf = getNameYobLeafAahaar( + sharedData.extractedFields.name, + sharedData.extractedFields.yob + ); + + const { + root: ofac_name_dob_smt_root, + closestleaf: ofac_name_dob_smt_leaf_key, + siblings: ofac_name_dob_smt_siblings, + } = generateSMTProof(nameAndDob_smt, namedob_leaf); + + const { + root: ofac_name_yob_smt_root, + closestleaf: ofac_name_yob_smt_leaf_key, + siblings: ofac_name_yob_smt_siblings, + } = generateSMTProof(nameAndYob_smt, nameyob_leaf); + const inputs = { - qrDataPadded: Uint8ArrayToCharArray(sharedData.qrDataPadded), - qrDataPaddedLength: sharedData.qrDataPaddedLen, - delimiterIndices: delimiterIndices, - signature: splitToWords(signature, BigInt(121), BigInt(17)), - pubKey: splitToWords(pubKey, BigInt(121), BigInt(17)), + attestation_id: '3', secret: secret, - photoEOI: photoEOI, + qrDataHash: BigInt(sharedData.qrHash).toString(), + gender: genderAscii.toString(), + yob: stringToAsciiArray(sharedData.extractedFields.yob), + mob: stringToAsciiArray(sharedData.extractedFields.mob), + dob: stringToAsciiArray(sharedData.extractedFields.dob), + name: formatInput(paddedName), + aadhaar_last_4digits: stringToAsciiArray(sharedData.extractedFields.aadhaarLast4Digits), + pincode: stringToAsciiArray(sharedData.extractedFields.pincode), + state: stringToAsciiArray(sharedData.extractedFields.state.padEnd(31, '\0')), + ph_no_last_4digits: stringToAsciiArray(sharedData.extractedFields.phoneNoLast4Digits), + photoHash: formatInput(BigInt(sharedData.photoHash)), + merkle_root: formatInput(BigInt(merkletree.root)), + leaf_depth: formatInput(leaf_depth), + path: formatInput(merkle_path), + siblings: formatInput(siblings), + ofac_name_dob_smt_leaf_key: formatInput(BigInt(ofac_name_dob_smt_leaf_key)), + ofac_name_dob_smt_root: formatInput(BigInt(ofac_name_dob_smt_root)), + ofac_name_dob_smt_siblings: formatInput(ofac_name_dob_smt_siblings), + ofac_name_yob_smt_leaf_key: formatInput(BigInt(ofac_name_yob_smt_leaf_key)), + ofac_name_yob_smt_root: formatInput(BigInt(ofac_name_yob_smt_root)), + ofac_name_yob_smt_siblings: formatInput(ofac_name_yob_smt_siblings), + selector, + minimumAge: formatInput(age - 2), + currentYear: formatInput(currentYear), + currentMonth: formatInput(currentMonth), + currentDay: formatInput(currentDay), + scope: formatInput(BigInt(scope)), + user_identifier: formatInput(BigInt(user_identifier)), + forbidden_countries_list: [...Array(120)].map((x) => '0'), }; return { @@ -449,3 +439,137 @@ export function prepareAadhaarRegisterTestData( commitment, }; } + +export function prepareAadhaarDiscloseData( + qrData: string, + identityTree: LeanIMT, + nameAndDob_smt: SMT, + nameAndYob_smt: SMT, + scope: string, + secret: string, + user_identifier: string, + discloseAttributes: { + dateOfBirth?: boolean; + name?: boolean; + gender?: boolean; + idNumber?: boolean; + issuingState?: boolean; + minimumAge?: number; + forbiddenCountriesListPacked?: string[]; + ofac?: boolean; + } +) { + const sharedData = processQRDataSimple(qrData); + + const { currentYear, currentMonth, currentDay } = calculateAge( + sharedData.extractedFields.dob, + sharedData.extractedFields.mob, + sharedData.extractedFields.yob + ); + + const genderAscii = stringToAsciiArray(sharedData.extractedFields.gender)[0]; + const nullifier = nullifierHash(sharedData.extractedFields); + const packedCommitment = computePackedCommitment(sharedData.extractedFields); + const commitment = computeCommitment( + BigInt(secret), + BigInt(sharedData.qrHash), + nullifier, + packedCommitment, + BigInt(sharedData.photoHash) + ); + + const paddedName = computePaddedName(sharedData.extractedFields.name); + + const index = findIndexInTree(identityTree, BigInt(commitment)); + const { + siblings, + path: merkle_path, + leaf_depth, + } = generateMerkleProof(identityTree, index, COMMITMENT_TREE_DEPTH); + + const namedob_leaf = getNameDobLeafAadhaar( + sharedData.extractedFields.name, + sharedData.extractedFields.yob, + sharedData.extractedFields.mob, + sharedData.extractedFields.dob + ); + const nameyob_leaf = getNameYobLeafAahaar( + sharedData.extractedFields.name, + sharedData.extractedFields.yob + ); + + const { + root: ofac_name_dob_smt_root, + closestleaf: ofac_name_dob_smt_leaf_key, + siblings: ofac_name_dob_smt_siblings, + } = generateSMTProof(nameAndDob_smt, namedob_leaf); + + const { + root: ofac_name_yob_smt_root, + closestleaf: ofac_name_yob_smt_leaf_key, + siblings: ofac_name_yob_smt_siblings, + } = generateSMTProof(nameAndYob_smt, nameyob_leaf); + + const selectorArr: AadhaarField[] = []; + if (discloseAttributes.dateOfBirth) { + selectorArr.push('YEAR_OF_BIRTH'); + selectorArr.push('MONTH_OF_BIRTH'); + selectorArr.push('DAY_OF_BIRTH'); + } + if (discloseAttributes.name) { + selectorArr.push('NAME'); + } + if (discloseAttributes.gender) { + selectorArr.push('GENDER'); + } + if (discloseAttributes.idNumber) { + selectorArr.push('AADHAAR_LAST_4_DIGITS'); + } + if (discloseAttributes.issuingState) { + selectorArr.push('STATE'); + } + if (discloseAttributes.ofac) { + selectorArr.push('OFAC_NAME_DOB_CHECK'); + selectorArr.push('OFAC_NAME_YOB_CHECK'); + } + + const selector = createSelector(selectorArr); + + const inputs = { + attestation_id: '3', + secret, + qrDataHash: BigInt(sharedData.qrHash).toString(), + gender: genderAscii.toString(), + yob: stringToAsciiArray(sharedData.extractedFields.yob), + mob: stringToAsciiArray(sharedData.extractedFields.mob), + dob: stringToAsciiArray(sharedData.extractedFields.dob), + name: formatInput(paddedName), + aadhaar_last_4digits: stringToAsciiArray(sharedData.extractedFields.aadhaarLast4Digits), + pincode: stringToAsciiArray(sharedData.extractedFields.pincode), + state: stringToAsciiArray(sharedData.extractedFields.state.padEnd(31, '\0')), + ph_no_last_4digits: stringToAsciiArray(sharedData.extractedFields.phoneNoLast4Digits), + photoHash: formatInput(BigInt(sharedData.photoHash)), + merkle_root: formatInput(BigInt(identityTree.root)), + leaf_depth: formatInput(leaf_depth), + path: formatInput(merkle_path), + siblings: formatInput(siblings), + ofac_name_dob_smt_leaf_key: formatInput(BigInt(ofac_name_dob_smt_leaf_key)), + ofac_name_dob_smt_root: formatInput(BigInt(ofac_name_dob_smt_root)), + ofac_name_dob_smt_siblings: formatInput(ofac_name_dob_smt_siblings), + ofac_name_yob_smt_leaf_key: formatInput(BigInt(ofac_name_yob_smt_leaf_key)), + ofac_name_yob_smt_root: formatInput(BigInt(ofac_name_yob_smt_root)), + ofac_name_yob_smt_siblings: formatInput(ofac_name_yob_smt_siblings), + selector, + minimumAge: formatInput(discloseAttributes.minimumAge ?? 0), + currentYear: formatInput(currentYear), + currentMonth: formatInput(currentMonth), + currentDay: formatInput(currentDay), + scope: formatInput(BigInt(scope)), + user_identifier: formatInput(BigInt(user_identifier)), + forbidden_countries_list: discloseAttributes.forbiddenCountriesListPacked + ? formatInput(formatCountriesList(discloseAttributes.forbiddenCountriesListPacked)) + : formatInput([...Array(120)].map((_) => '0')), + }; + + return inputs; +} From 55a0edcfccab18e02050afa8424490be2bf5eaab Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 16 Sep 2025 16:33:31 -0700 Subject: [PATCH 21/25] chore: update monorepo artifacts (#1079) * remove unneeded artifacts, skip building circuits * update md files --- .cursor/rules/technical-specification.mdc | 161 ------------- .cursorrules | 20 -- README.md | 1 - app/AGENTS.md | 2 - docs/nfc-implementation.md | 273 ---------------------- package.json | 6 +- 6 files changed, 3 insertions(+), 460 deletions(-) delete mode 100644 .cursor/rules/technical-specification.mdc delete mode 100644 docs/nfc-implementation.md diff --git a/.cursor/rules/technical-specification.mdc b/.cursor/rules/technical-specification.mdc deleted file mode 100644 index c48bba6ac..000000000 --- a/.cursor/rules/technical-specification.mdc +++ /dev/null @@ -1,161 +0,0 @@ -# Technical Specification: Identity Verification System - -## 1. Zero-Knowledge Proof Circuits - -### Register Circuit Implementation -- **Circuit Location**: `circuits/circuits/register/register.circom` -- **Purpose**: Generate identity commitments and nullifiers for passport/ID verification -- **Input Validation**: - - Passport number: normalized (whitespace/punctuation removed) - - Date of birth: ISO 8601 format (YYYY-MM-DD) - - Expiry date: ISO 8601 format (YYYY-MM-DD) - - Country code: ISO 3166-1 alpha-3 format -- **Output**: Poseidon hash commitment with domain separation `"register-v1"` - -### DSC Proof Circuit -- **Circuit Location**: `circuits/circuits/dsc/dsc.circom` -- **Purpose**: Validate Document Signer Certificate authenticity -- **Validation Steps**: - - Certificate chain verification (CSCA → DSC) - - Signature algorithm support (RSA-2048, ECDSA-P256) - - Revocation list checking via Merkle inclusion -- **Memory Constraints**: <300MB peak memory usage - -### Disclosure Proof Circuit -- **Circuit Location**: `circuits/circuits/disclose/vc_and_disclose.circom` -- **Purpose**: Selective attribute revelation with privacy preservation -- **Supported Disclosures**: - - Age verification: "olderThan" with day granularity - - Country verification: forbidden country checks - - Document type: passport vs EU ID card -- **Privacy**: No raw PII leaves device, only commitments - -## 2. Data Models & Structures - -### Identity Commitment Format -```typescript -interface IdentityCommitment { - commitment: string; // Poseidon hash - nullifier: string; // Domain-separated nullifier - timestamp: number; // UTC timestamp - version: string; // Circuit version - documentType: 'passport' | 'eu_id_card'; -} -``` - -### DSC Key Commitment -```typescript -interface DSCKeyCommitment { - publicKeyHash: string; // Poseidon hash of public key - certificateChain: string[]; // Certificate chain hashes - revocationStatus: boolean; // Revocation list inclusion - issuer: string; // Issuing country code -} -``` - -### Verification Configuration -```typescript -interface VerificationConfig { - circuitVersion: string; // Semantic versioning - complianceRules: ComplianceRule[]; - timeWindow: number; // 24-hour window in seconds - clockDrift: number; // ±5 minutes tolerance - trustAnchors: string[]; // Trusted certificate authority roots - revocationRoots: string[]; // Certificate revocation list roots - timeSource: string; // NTP server for time validation - nullifierScope: string; // Domain separation for nullifiers -} -``` - -## 3. Identity Verification Workflows - -### Passport Verification Path -1. **NFC Data Extraction**: Read passport chip via NFC -2. **MRZ Validation**: Parse and validate Machine Readable Zone -3. **DSC Verification**: Validate Document Signer Certificate -4. **Register Proof**: Generate identity commitment -5. **Compliance Check**: OFAC screening via zero-knowledge proofs -6. **Attestation**: Create privacy-preserving attestation - -### EU ID Card Verification Path -1. **Similar to passport** with EU-specific compliance checks -2. **Additional validation**: EU document format requirements -3. **Regional compliance**: EU-specific forbidden country lists - -### Cross-Chain Verification -- **Commitment linking**: Link commitments across different chains -- **Nullifier tracking**: Prevent duplicate registrations across chains -- **Attestation portability**: Share attestations across ecosystems - -## 4. Implementation Requirements - -### Cryptographic Standards -- **Hash Function**: Poseidon (optimized for ZK circuits) -- **Field Size**: BN254 scalar field (254 bits) -- **Key Sizes**: RSA-2048 minimum, ECDSA-P256 preferred -- **Randomness**: CSPRNG with entropy validation -- **Signature Scheme**: EdDSA over BabyJubJub (zk-friendly) -- **MRZ Validation**: Check digits validation for data integrity -- **Authentication**: Passive/active authentication modes -- **NFC Security**: Secure channel establishment with PACE/BAC - -### Performance Constraints -- **Proof Generation**: <60s on mid-tier mobile devices -- **Memory Usage**: <300MB peak memory -- **Bundle Size**: <2MB SDK bundle size -- **Startup Time**: <1s SDK initialization - -### Security Requirements -- **PII Protection**: No raw PII in logs or network traffic -- **Key Management**: Secure key storage with rotation -- **Audit Trails**: Privacy-preserving audit logs -- **Input Validation**: Comprehensive input sanitization -- **OFAC Anchoring**: Signed list snapshots with freshness guarantees -- **Cross-Chain Privacy**: Domain-separated nullifiers prevent linkability -- **Hardware Security**: Hardware keystore requirements for mobile -- **TLS Pinning**: Certificate pinning for secure communications -- **Telemetry Minimization**: Minimal data collection and transmission - -## 5. Error Handling & Recovery - -### Circuit Errors -- **Invalid Input**: Clear error codes for malformed data -- **Proof Failure**: Graceful degradation with retry logic -- **Memory Limits**: Client-side only - no server-side proving fallback -- **Timeout Handling**: Configurable timeouts with progress events - -### Network Errors -- **Retry Logic**: Exponential backoff with jitter -- **Offline Support**: Cached verification data -- **Graceful Degradation**: Fallback verification methods -- **Error Reporting**: Non-identifying error metadata - -## 6. Testing Requirements - -### Circuit Testing -- **Unit Tests**: Individual circuit component testing -- **Integration Tests**: End-to-end proof generation -- **Performance Tests**: Memory and timing validation -- **Security Tests**: Cryptographic validation - -### SDK Testing -- **Cross-Platform**: iOS, Android, Web compatibility -- **Memory Profiling**: Memory leak detection -- **Bundle Analysis**: Tree-shaking validation -- **Integration Testing**: App integration validation - -## 7. Deployment & Versioning - -### Circuit Versioning -- **Semantic Versioning**: MAJOR.MINOR.PATCH -- **Backward Compatibility**: MINOR versions maintain compatibility -- **Migration Strategy**: Graceful migration with deprecation windows -- **Rollback Plan**: Emergency rollback procedures - -### SDK Distribution -- **Package Management**: npm/yarn distribution -- **Version Compatibility**: Circuit version compatibility matrix -- **Documentation**: Comprehensive API documentation -- **Examples**: Integration examples and demos - -This specification provides concrete implementation guidance for the identity verification system with specific technical requirements, performance constraints, and development patterns. diff --git a/.cursorrules b/.cursorrules index d0cb43a51..7a2e7d399 100644 --- a/.cursorrules +++ b/.cursorrules @@ -48,7 +48,6 @@ ## Core Workflows 1. Document Verification Flow -- NFC chip data extraction and validation - Zero-knowledge proof generation for privacy - Multi-stage attestation verification - Cross-chain verification support @@ -128,25 +127,6 @@ This is a React Native identity verification app with NFC passport reading, zero - Test utilities in `tests/__setup__/databaseMocks.ts` - Mock database instance for testing -## NFC Implementation - -### Cross-Platform Architecture -- iOS: Custom PassportReader Swift module -- Android: Custom RNPassportReaderModule Kotlin implementation -- Unified JavaScript interface with platform detection - -### Authentication Methods -- MRZ Key: Derived from passport number, DOB, and expiry date -- CAN (Card Access Number): 6-digit number for PACE authentication -- PACE: Password Authenticated Connection Establishment -- BAC fallback when PACE fails - -### Error Handling -- Multiple BAC attempts with delays -- Graceful degradation from PACE to BAC -- Real-time status updates and haptic feedback -- Comprehensive error boundaries - ## Code Organization ### File Structure diff --git a/README.md b/README.md index fbd81c41d..ae42dc592 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,6 @@ For detailed development patterns and conventions, see: - **[Development Patterns](docs/development-patterns.md)** - React Native architecture, navigation, state management, and code organization - **[Testing Guide](docs/testing-guide.md)** - Jest configuration, mock patterns, testing strategies, and E2E testing -- **[NFC Implementation](docs/nfc-implementation.md)** - Cross-platform NFC passport reading, authentication methods, and error handling These guides provide comprehensive context for AI-assisted development with ChatGPT Codex, Cursor, and CodeRabbit AI. diff --git a/app/AGENTS.md b/app/AGENTS.md index c62a73e85..e4aa6d220 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -24,7 +24,6 @@ Before creating a PR for the mobile app: - [ ] iOS build succeeds: `yarn ios` (simulator) - [ ] Android build succeeds: `yarn android` (emulator/device) - [ ] Web build succeeds: `yarn web` -- [ ] NFC functionality tested (if applicable) - [ ] No sensitive data in logs (PII, credentials, tokens) ### AI Review Preparation @@ -52,7 +51,6 @@ After PR creation: ### Review Integration - [ ] Address CodeRabbitAI feedback - [ ] Resolve any security warnings -- [ ] Verify NFC/security features still work - [ ] Confirm no sensitive data exposed ## Recommended Workflow diff --git a/docs/nfc-implementation.md b/docs/nfc-implementation.md deleted file mode 100644 index faa5e3f07..000000000 --- a/docs/nfc-implementation.md +++ /dev/null @@ -1,273 +0,0 @@ -# NFC Implementation Guide - -## Architecture Overview - -### Cross-Platform Implementation -The NFC passport reading system uses platform-specific native modules with a unified JavaScript interface: - -- **iOS**: Custom PassportReader Swift module -- **Android**: Custom RNPassportReaderModule Kotlin implementation -- **JavaScript**: Unified interface with platform detection - -### Data Flow -1. User initiates scan with passport details (MRZ data) -2. Platform-specific native module handles NFC communication -3. Raw passport data returned to JavaScript -4. Data parsed and normalized for processing -5. Zero-knowledge proof generation and verification - -## iOS Implementation - -### PassportReader Module -The iOS implementation uses a custom Swift module with NFC capabilities: - -```swift -func readPassport( - password: String, - type: PACEPasswordType, - tags: [NFCISO7816Tag], - skipCA: Bool, - skipPACE: Bool, - useExtendedMode: Bool, - usePacePolling: Bool, - customDisplayMessage: ((NFCViewDisplayMessage) -> String?)? -) async throws -> Passport -``` - -### Authentication Methods -- **MRZ Key**: Derived from passport number, date of birth, and expiry date -- **CAN (Card Access Number)**: 6-digit number for PACE authentication -- **PACE**: Password Authenticated Connection Establishment - -### Error Handling -- Comprehensive try-catch blocks for each authentication step -- Graceful fallback from PACE to BAC when needed -- User-friendly error messages for different failure scenarios - -## Android Implementation - -### RNPassportReaderModule -The Android implementation uses a custom Kotlin module: - -```kotlin -@ReactMethod -fun scan(opts: ReadableMap, promise: Promise) { - // NFC adapter setup and validation - // Intent handling for tag discovery - // Async task execution for passport reading -} -``` - -### Authentication Flow -1. **PACE Authentication**: Primary method using CAN or MRZ key -2. **BAC Fallback**: Basic Access Control when PACE fails -3. **Retry Logic**: Multiple attempts with delays between retries - -### Intent Handling -- `onNewIntent` in MainActivity routes to RNPassportReaderModule -- Foreground dispatch for NFC tag discovery -- Proper lifecycle management (resume/pause) - -### Android-Specific Workarounds -On certain Android devices, NFC scanner fails to activate on app launch. The automatic workaround uses an event-driven approach: -- `scan()` method sets `shouldEnableNfcOnResume=true` -- Programmatically backgrounds the app using `moveTaskToBack(true)` -- Waits 500ms, then foregrounds it -- NFC enablement happens in `onHostResume()` when activity is properly in foreground - -## JavaScript Interface - -### Unified Scan Function -```typescript -export const scan = async (inputs: Inputs) => { - return Platform.OS === 'android' - ? await scanAndroid(inputs) - : await scanIOS(inputs); -}; -``` - -### Input Parameters -```typescript -interface Inputs { - passportNumber: string; - dateOfBirth: string; - dateOfExpiry: string; - canNumber?: string; - useCan?: boolean; - skipPACE?: boolean; - skipCA?: boolean; - extendedMode?: boolean; - usePacePolling?: boolean; -} -``` - -### Response Processing -- Platform-specific response parsing -- Data normalization for consistent format -- Error handling and validation - -## Authentication Methods - -### MRZ Key Generation -The MRZ key is derived from passport data: -```typescript -const mrzKey = getMRZKey( - passportNumber: string, - dateOfBirth: string, - dateOfExpiry: string -); -``` - -### PACE Authentication -- **Primary Method**: Uses CAN or MRZ key for authentication -- **Extended Mode**: Enhanced security features -- **Polling**: Optional polling mechanism for better reliability - -### BAC (Basic Access Control) -- **Fallback Method**: Used when PACE fails -- **Multiple Attempts**: Up to 3 attempts with delays -- **Error Recovery**: Graceful handling of authentication failures - -## Error Handling Patterns - -### Authentication Failures -- **Access Denied**: Invalid credentials or expired passport -- **BAC Denied**: Basic Access Control failure -- **PACE Exception**: Password Authenticated Connection Establishment failure -- **Card Exception**: General NFC communication issues - -### Retry Mechanisms -- Multiple BAC attempts with delays between retries -- Graceful degradation from PACE to BAC -- Timeout handling for slow operations -- User feedback during retry attempts - -### User Feedback -- Real-time status updates during scanning -- Haptic feedback for different states -- Clear error messages for troubleshooting -- Progress indicators for long operations - -## Performance Optimizations - -### Timeout Management -- Configurable timeouts for different operations -- Platform-specific timeout values -- Graceful timeout handling with user feedback - -### Memory Management -- Proper cleanup of NFC connections -- Resource disposal in finally blocks -- Memory leak prevention -- Component lifecycle management - -### Debug Logging -- Conditional debug logging based on environment -- Performance timing for scan operations -- Detailed error logging for troubleshooting -- Analytics integration for performance monitoring - -## Security Considerations - -### Data Protection -- Sensitive data not logged in production -- Secure storage of authentication keys -- Proper cleanup of sensitive data -- Memory protection for cryptographic operations - -### Certificate Validation -- Document Signer Certificate (DSC) validation -- Certificate chain verification -- Trust store management -- Certificate revocation checking - -### Privacy Features -- Zero-knowledge proof generation -- Selective attribute revelation -- Privacy-preserving age verification -- Identity commitment privacy - -## Testing Strategy - -### Mock Implementations -- Comprehensive mocks for testing without hardware -- Platform-specific mock data -- Error scenario testing -- Performance testing with simulated delays - -### Integration Testing -- Real device testing for critical paths -- Platform-specific test scenarios -- Performance benchmarking -- Security testing with realistic, synthetic passport data (NEVER real user data) - -### Test Data -- Mock passport data for unit tests -- Test certificates for validation -- Error scenarios for edge case testing -- Performance benchmarks for optimization - -## Platform-Specific Considerations - -### iOS -- NFC capabilities require iOS 13+ -- Background NFC reading limitations -- Privacy permissions and user consent -- App Store review requirements - -### Android -- NFC hardware requirements -- Permission handling -- Background processing limitations -- Device-specific workarounds - -### Web -- No NFC support (fallback to other methods) -- Alternative authentication flows -- Cross-platform compatibility - -## Common Issues and Solutions - -### NFC Not Detected -- Check device NFC capabilities -- Verify NFC is enabled in settings -- Ensure app has proper permissions -- Try device-specific workarounds - -### Authentication Failures -- Verify passport data accuracy -- Check passport expiration -- Try alternative authentication methods -- Implement retry logic with delays - -### Performance Issues -- Optimize timeout values -- Implement proper cleanup -- Monitor memory usage -- Profile NFC operations - -## Best Practices - -### Error Handling -- Always implement comprehensive error handling -- Provide user-friendly error messages -- Log errors for debugging -- Implement graceful degradation - -### Performance -- Optimize timeout values for your use case -- Implement proper resource cleanup -- Monitor memory usage -- Profile NFC operations - -### Security -- Never log sensitive data -- Implement proper certificate validation -- Use secure storage for keys -- Follow platform security guidelines - -### User Experience -- Provide clear feedback during operations -- Implement haptic feedback -- Show progress indicators -- Handle edge cases gracefully diff --git a/package.json b/package.json index 43312863f..f3b7308f6 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "packages/mobile-sdk-alpha/demo-app" ], "scripts": { - "build": "yarn workspaces foreach --topological-dev --parallel --exclude @selfxyz/contracts -i --all run build", + "build": "yarn workspaces foreach --topological-dev --parallel --exclude @selfxyz/contracts --exclude @selfxyz/circuits -i --all run build", "check:versions": "node scripts/check-package-versions.mjs", "format": "SKIP_BUILD_DEPS=1 yarn format:root && yarn format:github && SKIP_BUILD_DEPS=1 yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run format", "format:github": "yarn prettier --parser yaml --write .github/**/*.yml --single-quote false", @@ -31,11 +31,11 @@ "resolutions": { "@babel/core": "^7.28.4", "@babel/runtime": "^7.28.4", - "@typescript-eslint/eslint-plugin": "^8.43.0", - "@typescript-eslint/parser": "^8.43.0", "@types/node": "^22.18.3", "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.43.0", + "@typescript-eslint/parser": "^8.43.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-native": "0.76.9" From 57a40a8373c5467ce23888d1961fddeecfc7fb82 Mon Sep 17 00:00:00 2001 From: ayman Date: Wed, 17 Sep 2025 16:09:17 +0530 Subject: [PATCH 22/25] chore: update hub contract address --- sdk/core/src/SelfBackendVerifier.ts | 2 +- sdk/sdk-go/verifier.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/core/src/SelfBackendVerifier.ts b/sdk/core/src/SelfBackendVerifier.ts index b898184d8..e3d8c7e63 100644 --- a/sdk/core/src/SelfBackendVerifier.ts +++ b/sdk/core/src/SelfBackendVerifier.ts @@ -27,7 +27,7 @@ const CELO_MAINNET_RPC_URL = 'https://forno.celo.org'; const CELO_TESTNET_RPC_URL = 'https://alfajores-forno.celo-testnet.org'; const IDENTITY_VERIFICATION_HUB_ADDRESS = '0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF'; -const IDENTITY_VERIFICATION_HUB_ADDRESS_STAGING = '0x68c931C9a534D37aa78094877F46fE46a49F1A51'; +const IDENTITY_VERIFICATION_HUB_ADDRESS_STAGING = '0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74'; export class SelfBackendVerifier { protected scope: string; diff --git a/sdk/sdk-go/verifier.go b/sdk/sdk-go/verifier.go index b336a42b0..d04f64a32 100644 --- a/sdk/sdk-go/verifier.go +++ b/sdk/sdk-go/verifier.go @@ -21,7 +21,7 @@ const ( CELO_TESTNET_RPC_URL = "https://alfajores-forno.celo-testnet.org" IDENTITY_VERIFICATION_HUB_ADDRESS = "0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF" - IDENTITY_VERIFICATION_HUB_ADDRESS_STAGING = "0x68c931C9a534D37aa78094877F46fE46a49F1A51" + IDENTITY_VERIFICATION_HUB_ADDRESS_STAGING = "0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74" ) // ConfigMismatch represents different types of configuration validation errors From 9a676349a7ea25f9a27c4cdf3584a66eee7bc67d Mon Sep 17 00:00:00 2001 From: ayman Date: Wed, 17 Sep 2025 16:16:45 +0530 Subject: [PATCH 23/25] format --- sdk/core/src/SelfBackendVerifier.ts | 16 ++++++-- sdk/core/src/utils/constants.ts | 4 +- sdk/core/src/utils/id.ts | 62 ++++++++++++++++++----------- sdk/core/src/utils/proof.ts | 2 +- 4 files changed, 54 insertions(+), 30 deletions(-) diff --git a/sdk/core/src/SelfBackendVerifier.ts b/sdk/core/src/SelfBackendVerifier.ts index e3d8c7e63..c62224f59 100644 --- a/sdk/core/src/SelfBackendVerifier.ts +++ b/sdk/core/src/SelfBackendVerifier.ts @@ -221,9 +221,19 @@ export class SelfBackendVerifier { let circuitTimestampMm: number[]; let circuitTimestampDd: number[]; if (attestationId === 3) { - circuitTimestampYy = String(publicSignals[discloseIndices[attestationId].currentDateIndex]).split('').map(Number); - circuitTimestampMm = String(publicSignals[discloseIndices[attestationId].currentDateIndex + 1]).split('').map(Number); - circuitTimestampDd = String(publicSignals[discloseIndices[attestationId].currentDateIndex + 2]).split('').map(Number); + circuitTimestampYy = String(publicSignals[discloseIndices[attestationId].currentDateIndex]) + .split('') + .map(Number); + circuitTimestampMm = String( + publicSignals[discloseIndices[attestationId].currentDateIndex + 1] + ) + .split('') + .map(Number); + circuitTimestampDd = String( + publicSignals[discloseIndices[attestationId].currentDateIndex + 2] + ) + .split('') + .map(Number); } else { circuitTimestampYy = [ 2, diff --git a/sdk/core/src/utils/constants.ts b/sdk/core/src/utils/constants.ts index cce86f397..b16e376d0 100644 --- a/sdk/core/src/utils/constants.ts +++ b/sdk/core/src/utils/constants.ts @@ -27,7 +27,7 @@ export const discloseIndices = { userIdentifierIndex: 20, passportNoSmtRootIndex: 99, }, - 3: { + 3: { revealedDataPackedIndex: 2, forbiddenCountriesListPackedIndex: 6, nullifierIndex: 0, @@ -39,7 +39,7 @@ export const discloseIndices = { scopeIndex: 17, userIdentifierIndex: 18, passportNoSmtRootIndex: 99, - } + }, } as const; type RevealedDataFields = diff --git a/sdk/core/src/utils/id.ts b/sdk/core/src/utils/id.ts index 61fb5adea..001dc80ee 100644 --- a/sdk/core/src/utils/id.ts +++ b/sdk/core/src/utils/id.ts @@ -50,22 +50,27 @@ export const formatRevealedDataPacked = ( nationality = 'IND'; } else { nationality = revealedDataPackedString - .subarray( - revealedDataIndices[attestationId].nationalityStart, - revealedDataIndices[attestationId].nationalityEnd + 1 - ) - .toString('utf-8'); + .subarray( + revealedDataIndices[attestationId].nationalityStart, + revealedDataIndices[attestationId].nationalityEnd + 1 + ) + .toString('utf-8'); } let dateOfBirth: string; if (attestationId === 3) { - dateOfBirth = new Array(revealedDataPackedString.subarray( - revealedDataIndices[attestationId].dateOfBirthStart, - revealedDataIndices[attestationId].dateOfBirthEnd + 1 - )).map(Number).map(String).join(''); + dateOfBirth = new Array( + revealedDataPackedString.subarray( + revealedDataIndices[attestationId].dateOfBirthStart, + revealedDataIndices[attestationId].dateOfBirthEnd + 1 + ) + ) + .map(Number) + .map(String) + .join(''); } else { - dateOfBirth = revealedDataPackedString - .subarray( - revealedDataIndices[attestationId].dateOfBirthStart, + dateOfBirth = revealedDataPackedString + .subarray( + revealedDataIndices[attestationId].dateOfBirthStart, revealedDataIndices[attestationId].dateOfBirthEnd + 1 ) .toString('utf-8'); @@ -89,27 +94,36 @@ export const formatRevealedDataPacked = ( } let olderThan: string; if (attestationId === 3) { - olderThan = revealedDataPackedString.subarray( - revealedDataIndices[attestationId].olderThanStart, - revealedDataIndices[attestationId].olderThanEnd + 1 - ).toString('utf-8'); + olderThan = revealedDataPackedString + .subarray( + revealedDataIndices[attestationId].olderThanStart, + revealedDataIndices[attestationId].olderThanEnd + 1 + ) + .toString('utf-8'); } else { - olderThan = - revealedDataPackedString.subarray( + olderThan = revealedDataPackedString + .subarray( revealedDataIndices[attestationId].olderThanStart, revealedDataIndices[attestationId].olderThanEnd + 1 - )[0].toString().padStart(2, '0'); + )[0] + .toString() + .padStart(2, '0'); } - console.log('ofac', revealedDataPackedString.subarray( - revealedDataIndices[attestationId].ofacStart, - revealedDataIndices[attestationId].ofacEnd + 1 - )); + console.log( + 'ofac', + revealedDataPackedString.subarray( + revealedDataIndices[attestationId].ofacStart, + revealedDataIndices[attestationId].ofacEnd + 1 + ) + ); const ofac = Array.from( revealedDataPackedString.subarray( revealedDataIndices[attestationId].ofacStart, revealedDataIndices[attestationId].ofacEnd + 1 ) - ).map(Boolean).map((x) => !x); + ) + .map(Boolean) + .map((x) => !x); if (ofac.length < 3) { ofac.unshift(false); diff --git a/sdk/core/src/utils/proof.ts b/sdk/core/src/utils/proof.ts index 4d7bcd4ae..a77d13685 100644 --- a/sdk/core/src/utils/proof.ts +++ b/sdk/core/src/utils/proof.ts @@ -27,7 +27,7 @@ export function getRevealedDataPublicSignalsLength(attestationId: AttestationId) export const bytesCount: Record = { 1: [31, 31, 31], 2: [31, 31, 31, 1], - 3: [31, 31, 31, 26] + 3: [31, 31, 31, 26], }; /** From 09f1b90beca1a3695d3a1f23b72841814b841d2b Mon Sep 17 00:00:00 2001 From: vishal Date: Wed, 17 Sep 2025 16:34:55 +0530 Subject: [PATCH 24/25] fix: add aadhaar in AllIds --- sdk/sdk-go/utils.go | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/sdk-go/utils.go b/sdk/sdk-go/utils.go index 1334fc73a..cad548f65 100644 --- a/sdk/sdk-go/utils.go +++ b/sdk/sdk-go/utils.go @@ -180,6 +180,7 @@ var RevealedDataIndices = map[AttestationId]RevealedDataIndicesEntry{ var AllIds = map[AttestationId]bool{ Passport: true, EUCard: true, + Aadhaar: true, } // BytesCount maps attestation IDs to their respective byte counts From 0428ac487e1509dc11f8c98e2e1772c1152243dc Mon Sep 17 00:00:00 2001 From: ayman Date: Wed, 17 Sep 2025 16:42:47 +0530 Subject: [PATCH 25/25] chore: bump to v1.1.0-beta --- sdk/core/package.json | 2 +- sdk/core/src/utils/id.ts | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/sdk/core/package.json b/sdk/core/package.json index 169c53780..d1b414a15 100644 --- a/sdk/core/package.json +++ b/sdk/core/package.json @@ -1,6 +1,6 @@ { "name": "@selfxyz/core", - "version": "1.0.8", + "version": "1.1.0-beta.0", "repository": { "type": "git", "url": "https://github.com/selfxyz/self" diff --git a/sdk/core/src/utils/id.ts b/sdk/core/src/utils/id.ts index 001dc80ee..311102904 100644 --- a/sdk/core/src/utils/id.ts +++ b/sdk/core/src/utils/id.ts @@ -109,13 +109,6 @@ export const formatRevealedDataPacked = ( .toString() .padStart(2, '0'); } - console.log( - 'ofac', - revealedDataPackedString.subarray( - revealedDataIndices[attestationId].ofacStart, - revealedDataIndices[attestationId].ofacEnd + 1 - ) - ); const ofac = Array.from( revealedDataPackedString.subarray( revealedDataIndices[attestationId].ofacStart,