Fix missing DSC parsing for passport/id_card (#1498)

* Handle missing parsed DSC before fetching

* address cr feedback

* formatting

* fix bugs
This commit is contained in:
Justin Hernandez
2026-03-10 09:34:19 -07:00
committed by GitHub
parent d7c1556ddc
commit d815e9f0d3
2 changed files with 372 additions and 44 deletions

View File

@@ -1045,17 +1045,30 @@ export const useProvingStore = create<ProvingState>((set, get) => {
set({ passportData, secret, env });
set({ circuitType });
// Skip parsing for disclosure if passport is already parsed
// Only skip parsing when the document has already been parsed for non-DSC circuits.
// Re-parsing would overwrite the alternative CSCA used during registration and is unnecessary
// skip also the register circuit as the passport already got parsed in during the dsc step
console.log('circuitType', circuitType);
if (circuitType !== 'dsc') {
console.log('skipping id document parsing');
actor.send({ type: 'FETCH_DATA' });
selfClient.trackEvent(ProofEvents.FETCH_DATA_STARTED);
} else {
// for already parsed passports or ID cards.
// Aadhaar and KYC documents do not require DSC parsing at all.
const needsDscParsing =
passportData.documentCategory === 'passport' || passportData.documentCategory === 'id_card';
const hasParsedDsc = needsDscParsing && Boolean(passportData.dsc_parsed?.authorityKeyIdentifier);
if (circuitType === 'dsc' && !needsDscParsing) {
console.error(`DSC circuit is not supported for ${passportData.documentCategory} documents`);
selfClient.trackEvent(ProofEvents.PROOF_FAILED, {
message: `DSC circuit not supported for ${passportData.documentCategory}`,
});
actor.send({ type: 'ERROR' });
return;
}
const shouldParseDocument = circuitType === 'dsc' || (needsDscParsing && !hasParsedDsc);
if (shouldParseDocument) {
actor.send({ type: 'PARSE_ID_DOCUMENT' });
selfClient.trackEvent(ProofEvents.PARSE_ID_DOCUMENT_STARTED);
} else {
actor.send({ type: 'FETCH_DATA' });
}
},
@@ -1158,14 +1171,15 @@ export const useProvingStore = create<ProvingState>((set, get) => {
switch (passportData.documentCategory) {
case 'passport':
case 'id_card':
if (!passportData?.dsc_parsed) {
selfClient.logProofEvent('error', 'Missing parsed DSC', context, {
if (!passportData?.dsc_parsed?.authorityKeyIdentifier) {
const docType = passportData.documentCategory;
selfClient.logProofEvent('error', `Missing parsed DSC in ${docType} data`, context, {
failure: 'PROOF_FAILED_DATA_FETCH',
duration_ms: Date.now() - startTime,
});
console.error('Missing parsed DSC in passport data');
console.error(`Missing parsed DSC in ${docType} data`);
selfClient.trackEvent(ProofEvents.FETCH_DATA_FAILED, {
message: 'Missing parsed DSC in passport data',
message: `Missing parsed DSC in ${docType} data`,
});
actor!.send({ type: 'FETCH_ERROR' });
return;

View File

@@ -6,6 +6,7 @@ import type { SelfClient } from '../../src';
import { ProofEvents } from '../../src/constants/analytics';
import * as documentUtils from '../../src/documents/utils';
import { useProvingStore } from '../../src/proving/provingMachine';
import { fetchAllTreesAndCircuits } from '../../src/stores';
import { useProtocolStore } from '../../src/stores/protocolStore';
import { useSelfAppStore } from '../../src/stores/selfAppStore';
import { actorMock } from './actorMock';
@@ -36,46 +37,359 @@ vitest.mock('../../src/stores', async () => {
};
});
describe('startFetchingData', () => {
let mockSelfClient: SelfClient;
beforeEach(async () => {
const createMockSelfClient = () =>
({
getPrivateKey: vitest.fn().mockResolvedValue('secret'),
trackEvent: vitest.fn(),
logProofEvent: vitest.fn(),
getSelfAppState: () => useSelfAppStore.getState(),
getProvingState: () => useProvingStore.getState(),
getProtocolState: () => useProtocolStore.getState(),
}) as unknown as SelfClient;
function mockDocument(data: Record<string, unknown>) {
vitest.spyOn(documentUtils, 'loadSelectedDocument').mockResolvedValue({ data } as any);
}
describe('init parsing decision', () => {
beforeEach(() => {
vitest.clearAllMocks();
const loadSelectedDocumentSpy = vitest.spyOn(documentUtils, 'loadSelectedDocument');
loadSelectedDocumentSpy.mockResolvedValue({
data: {
documentCategory: 'passport',
mock: false,
dsc_parsed: { authorityKeyIdentifier: 'key' } as any,
},
} as any);
// Create mock selfClient
mockSelfClient = {
getPrivateKey: vitest.fn().mockResolvedValue('secret'), // or mock-secret?
trackEvent: vitest.fn(),
logProofEvent: vitest.fn(),
getSelfAppState: () => useSelfAppStore.getState(),
getProvingState: () => useProvingStore.getState(),
getProtocolState: () => useProtocolStore.getState(),
} as unknown as SelfClient;
await useProvingStore.getState().init(mockSelfClient, 'register');
actorMock.send.mockClear();
useProvingStore.setState({
passportData: { documentCategory: 'passport', mock: false },
env: 'prod',
} as any);
useProvingStore.setState({ circuitType: null, passportData: null, env: null });
});
it('emits FETCH_ERROR when dsc_parsed is missing', async () => {
describe('passport', () => {
it('skips parsing for disclose when dsc_parsed has authorityKeyIdentifier', async () => {
const client = createMockSelfClient();
mockDocument({
documentCategory: 'passport',
mock: false,
dsc_parsed: { authorityKeyIdentifier: 'key' },
});
await useProvingStore.getState().init(client, 'disclose');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_DATA' });
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' });
expect(client.trackEvent).not.toHaveBeenCalledWith(ProofEvents.PARSE_ID_DOCUMENT_STARTED);
});
it('parses for disclose when dsc_parsed is missing', async () => {
const client = createMockSelfClient();
mockDocument({ documentCategory: 'passport', mock: false });
await useProvingStore.getState().init(client, 'disclose');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' });
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_DATA' });
expect(client.trackEvent).toHaveBeenCalledWith(ProofEvents.PARSE_ID_DOCUMENT_STARTED);
});
it('parses when dsc_parsed exists but authorityKeyIdentifier is missing', async () => {
const client = createMockSelfClient();
mockDocument({
documentCategory: 'passport',
mock: false,
dsc_parsed: { issuer: 'some-issuer' },
});
await useProvingStore.getState().init(client, 'disclose');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' });
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_DATA' });
});
it('skips parsing for register when dsc_parsed has authorityKeyIdentifier', async () => {
const client = createMockSelfClient();
mockDocument({
documentCategory: 'passport',
mock: false,
dsc_parsed: { authorityKeyIdentifier: 'key' },
});
await useProvingStore.getState().init(client, 'register');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_DATA' });
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' });
});
it('parses for register when dsc_parsed is missing', async () => {
const client = createMockSelfClient();
mockDocument({ documentCategory: 'passport', mock: false });
await useProvingStore.getState().init(client, 'register');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' });
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_DATA' });
});
});
describe('id_card', () => {
it('skips parsing for disclose when dsc_parsed has authorityKeyIdentifier', async () => {
const client = createMockSelfClient();
mockDocument({
documentCategory: 'id_card',
mock: false,
dsc_parsed: { authorityKeyIdentifier: 'key' },
});
await useProvingStore.getState().init(client, 'disclose');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_DATA' });
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' });
});
it('parses when dsc_parsed exists but authorityKeyIdentifier is missing', async () => {
const client = createMockSelfClient();
mockDocument({
documentCategory: 'id_card',
mock: false,
dsc_parsed: { issuer: 'some-issuer' },
});
await useProvingStore.getState().init(client, 'disclose');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' });
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_DATA' });
});
it('parses for register when dsc_parsed is missing', async () => {
const client = createMockSelfClient();
mockDocument({ documentCategory: 'id_card', mock: false });
await useProvingStore.getState().init(client, 'register');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' });
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_DATA' });
});
});
describe('dsc circuit', () => {
it('always parses even when dsc_parsed already exists', async () => {
const client = createMockSelfClient();
mockDocument({
documentCategory: 'passport',
mock: false,
dsc_parsed: { authorityKeyIdentifier: 'key' },
});
await useProvingStore.getState().init(client, 'dsc');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' });
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_DATA' });
expect(client.trackEvent).toHaveBeenCalledWith(ProofEvents.PARSE_ID_DOCUMENT_STARTED);
});
it('parses when dsc_parsed is missing', async () => {
const client = createMockSelfClient();
mockDocument({ documentCategory: 'passport', mock: false });
await useProvingStore.getState().init(client, 'dsc');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' });
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_DATA' });
});
it('rejects dsc circuit for aadhaar documents', async () => {
const client = createMockSelfClient();
mockDocument({ documentCategory: 'aadhaar', mock: false });
await useProvingStore.getState().init(client, 'dsc');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'ERROR' });
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' });
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_DATA' });
expect(client.trackEvent).toHaveBeenCalledWith(ProofEvents.PROOF_FAILED, {
message: 'DSC circuit not supported for aadhaar',
});
});
it('rejects dsc circuit for kyc documents', async () => {
const client = createMockSelfClient();
mockDocument({ documentCategory: 'kyc', mock: false });
await useProvingStore.getState().init(client, 'dsc');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'ERROR' });
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' });
expect(client.trackEvent).toHaveBeenCalledWith(ProofEvents.PROOF_FAILED, {
message: 'DSC circuit not supported for kyc',
});
});
});
describe('aadhaar', () => {
it('skips parsing for disclose (does not require DSC)', async () => {
const client = createMockSelfClient();
mockDocument({ documentCategory: 'aadhaar', mock: false });
await useProvingStore.getState().init(client, 'disclose');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_DATA' });
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' });
expect(client.trackEvent).not.toHaveBeenCalledWith(ProofEvents.PARSE_ID_DOCUMENT_STARTED);
});
it('skips parsing for register (does not require DSC)', async () => {
const client = createMockSelfClient();
mockDocument({ documentCategory: 'aadhaar', mock: false });
await useProvingStore.getState().init(client, 'register');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_DATA' });
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' });
});
});
describe('kyc', () => {
it('skips parsing for disclose (does not require DSC)', async () => {
const client = createMockSelfClient();
mockDocument({ documentCategory: 'kyc', mock: false });
await useProvingStore.getState().init(client, 'disclose');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_DATA' });
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'PARSE_ID_DOCUMENT' });
expect(client.trackEvent).not.toHaveBeenCalledWith(ProofEvents.PARSE_ID_DOCUMENT_STARTED);
});
});
});
describe('startFetchingData', () => {
let mockSelfClient: SelfClient;
/** Run init() with a valid passport so the actor is ready, then clear mocks
* and set passportData to whatever the individual test needs. */
async function setupForFetch(passportData: Record<string, unknown>) {
vitest.clearAllMocks();
mockDocument({
documentCategory: 'passport',
mock: false,
dsc_parsed: { authorityKeyIdentifier: 'key' },
});
mockSelfClient = createMockSelfClient();
await useProvingStore.getState().init(mockSelfClient, 'register');
actorMock.send.mockClear();
(mockSelfClient.trackEvent as ReturnType<typeof vitest.fn>).mockClear();
(mockSelfClient.logProofEvent as ReturnType<typeof vitest.fn>).mockClear();
useProvingStore.setState({ passportData, env: 'prod' } as any);
}
it('emits FETCH_ERROR when dsc_parsed is missing for passport', async () => {
await setupForFetch({ documentCategory: 'passport', mock: false });
await useProvingStore.getState().startFetchingData(mockSelfClient);
expect(mockSelfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_STARTED);
expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_ERROR' });
expect(mockSelfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_FAILED, {
message: 'Missing parsed DSC in passport data',
});
expect(actorMock.send).not.toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' });
});
it('emits FETCH_ERROR when dsc_parsed exists but authorityKeyIdentifier is missing', async () => {
await setupForFetch({
documentCategory: 'passport',
mock: false,
dsc_parsed: { issuer: 'some-issuer' },
});
await useProvingStore.getState().startFetchingData(mockSelfClient);
expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_ERROR' });
expect(mockSelfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_FAILED, {
message: 'Missing parsed DSC in passport data',
});
});
it('emits FETCH_ERROR when dsc_parsed is missing for id_card', async () => {
await setupForFetch({ documentCategory: 'id_card', mock: false });
await useProvingStore.getState().startFetchingData(mockSelfClient);
expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_ERROR' });
expect(mockSelfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_FAILED, {
message: 'Missing parsed DSC in id_card data',
});
});
it('calls fetchAllTreesAndCircuits for passport with valid dsc_parsed', async () => {
await setupForFetch({
documentCategory: 'passport',
mock: false,
dsc_parsed: { authorityKeyIdentifier: 'test-aki-123' },
});
await useProvingStore.getState().startFetchingData(mockSelfClient);
expect(fetchAllTreesAndCircuits).toHaveBeenCalledWith(mockSelfClient, 'passport', 'prod', 'test-aki-123');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' });
expect(mockSelfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_SUCCESS);
});
it('calls fetchAllTreesAndCircuits for id_card with valid dsc_parsed', async () => {
await setupForFetch({
documentCategory: 'id_card',
mock: false,
dsc_parsed: { authorityKeyIdentifier: 'card-aki-456' },
});
await useProvingStore.getState().startFetchingData(mockSelfClient);
expect(fetchAllTreesAndCircuits).toHaveBeenCalledWith(mockSelfClient, 'id_card', 'prod', 'card-aki-456');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' });
});
it('calls aadhaar.fetch_all for aadhaar documents', async () => {
const mockFetchAll = vitest.fn().mockResolvedValue(undefined);
const client = createMockSelfClient();
(client as any).getProtocolState = () => ({
aadhaar: { fetch_all: mockFetchAll },
});
// Bootstrap actor via init
vitest.clearAllMocks();
mockDocument({ documentCategory: 'aadhaar', mock: false });
(client as any).getPrivateKey = vitest.fn().mockResolvedValue('secret');
await useProvingStore.getState().init(client, 'disclose');
actorMock.send.mockClear();
(client.trackEvent as ReturnType<typeof vitest.fn>).mockClear();
useProvingStore.setState({
passportData: { documentCategory: 'aadhaar', mock: false },
env: 'prod',
} as any);
await useProvingStore.getState().startFetchingData(client);
expect(mockFetchAll).toHaveBeenCalledWith('prod');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' });
expect(fetchAllTreesAndCircuits).not.toHaveBeenCalled();
});
it('calls kyc.fetch_all for kyc documents', async () => {
const mockFetchAll = vitest.fn().mockResolvedValue(undefined);
const client = createMockSelfClient();
(client as any).getProtocolState = () => ({
kyc: { fetch_all: mockFetchAll },
});
vitest.clearAllMocks();
mockDocument({ documentCategory: 'kyc', mock: false });
(client as any).getPrivateKey = vitest.fn().mockResolvedValue('secret');
await useProvingStore.getState().init(client, 'disclose');
actorMock.send.mockClear();
(client.trackEvent as ReturnType<typeof vitest.fn>).mockClear();
useProvingStore.setState({
passportData: { documentCategory: 'kyc', mock: false },
env: 'prod',
} as any);
await useProvingStore.getState().startFetchingData(client);
expect(mockFetchAll).toHaveBeenCalledWith('prod');
expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' });
expect(fetchAllTreesAndCircuits).not.toHaveBeenCalled();
});
});