Move self app store to mobile sdk (#1040)

This commit is contained in:
Aaron DeRuvo
2025-09-11 17:30:01 +02:00
committed by GitHub
parent f416211037
commit 1f362b33ce
35 changed files with 968 additions and 680 deletions

View File

@@ -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"
},

View File

@@ -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';

View File

@@ -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';

View File

@@ -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');
}
});
}

View File

@@ -3,3 +3,4 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
export { useProtocolStore } from './protocolStore';
export { useSelfAppStore } from './selfAppStore';

View File

@@ -0,0 +1,147 @@
// 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 { Socket } from 'socket.io-client';
import socketIo from 'socket.io-client';
import { create } from 'zustand';
import { WS_DB_RELAYER } from '@selfxyz/common/constants';
import type { SelfApp } from '@selfxyz/common/utils/appType';
interface SelfAppState {
selfApp: SelfApp | null;
sessionId: string | null;
socket: Socket | null;
startAppListener: (sessionId: string) => void;
cleanSelfApp: () => void;
setSelfApp: (selfApp: SelfApp | null) => void;
_initSocket: (sessionId: string) => Socket;
handleProofResult: (proof_verified: boolean, error_code?: string, reason?: string) => void;
}
export const useSelfAppStore = create<SelfAppState>((set, get) => ({
selfApp: null,
sessionId: null,
socket: null,
_initSocket: (sessionId: string): Socket => {
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.
const socket = socketIo(socketUrl, {
path: '/',
transports: ['websocket'],
forceNew: true, // Ensure a new connection is established
query: {
sessionId,
clientType: 'mobile',
},
});
return socket;
},
setSelfApp: (selfApp: SelfApp | null) => {
set({ selfApp });
},
startAppListener: (sessionId: string) => {
const currentSocket = get().socket;
// If a socket connection exists for a different session, disconnect it.
if (currentSocket && get().sessionId !== sessionId) {
currentSocket.disconnect();
set({ socket: null, sessionId: null, selfApp: null });
} else if (currentSocket && get().sessionId === sessionId) {
return; // Avoid reconnecting if already connected with the same session
}
try {
const socket = get()._initSocket(sessionId);
set({ socket, sessionId });
socket.on('connect', () => {});
// 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);
// Basic validation
if (!appData || typeof appData !== 'object' || !appData.sessionId) {
console.error('[SelfAppStore] Invalid app data received:', appData);
// Optionally clear the app data or handle the error appropriately
set({ selfApp: null });
return;
}
if (appData.sessionId !== get().sessionId) {
console.warn(
`[SelfAppStore] Received SelfApp for session ${
appData.sessionId
}, but current session is ${get().sessionId}. Ignoring.`,
);
return;
}
set({ selfApp: appData });
} catch (error) {
console.error('[SelfAppStore] Error processing app data:', error);
set({ selfApp: null }); // Clear app data on parsing error
}
});
socket.on('connect_error', error => {
console.error('[SelfAppStore] Mobile WS connection error:', error);
// Clean up on connection error
get().cleanSelfApp();
});
socket.on('error', error => {
console.error('[SelfAppStore] Mobile WS error:', error);
// Consider if cleanup is needed here as well
});
socket.on('disconnect', (_reason: string) => {
// Prevent cleaning up if disconnect was initiated by cleanSelfApp
if (get().socket === socket) {
set({ socket: null, sessionId: null, selfApp: null });
}
});
} catch (error) {
console.error('[SelfAppStore] Exception in startAppListener:', error);
get().cleanSelfApp(); // Clean up on exception
}
},
cleanSelfApp: () => {
const socket = get().socket;
if (socket) {
socket.disconnect();
}
// Reset state
set({ selfApp: null, sessionId: null, socket: null });
},
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.');
return;
}
if (proof_verified) {
socket.emit('proof_verified', {
session_id: sessionId,
});
} else {
socket.emit('proof_generation_failed', {
session_id: sessionId,
error_code,
reason,
});
}
},
}));

View File

@@ -0,0 +1,148 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
/**
* @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<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<<<<L898902C36UTO7408122F1204159ZE184226B<<<<',
dsc: '',
eContent: [],
signedAttr: [],
encryptedDigest: [],
passportMetadata: {
dataGroups: 'dg1',
dg1Size: 100,
dg1HashSize: 32,
dg1HashFunction: 'sha256',
dg1HashOffset: 0,
dgPaddingBytes: 0,
eContentSize: 100,
eContentHashFunction: 'sha256',
eContentHashOffset: 0,
signedAttrSize: 100,
signedAttrHashFunction: 'sha256',
signatureAlgorithm: 'rsa',
saltLength: 32,
curveOrExponent: '65537',
signatureAlgorithmBits: 0,
countryCode: '',
cscaFound: false,
cscaHashFunction: '',
cscaSignatureAlgorithm: '',
cscaSaltLength: 0,
cscaCurveOrExponent: '',
cscaSignatureAlgorithmBits: 0,
dsc: '',
csca: '',
},
dsc_parsed: {
tbsBytes: new Array(100).fill(1),
signatureAlgorithm: 'rsa',
publicKeyAlgorithm: 'rsa',
publicKeyDetails: {
modulus: '12345',
exponent: '65537',
},
signature: new Array(100).fill(1),
} as any,
csca_parsed: {
tbsBytes: new Array(100).fill(1),
signatureAlgorithm: 'rsa',
publicKeyAlgorithm: 'rsa',
publicKeyDetails: {
modulus: '12345',
exponent: '65537',
},
signature: new Array(100).fill(1),
} as any,
documentType: 'passport',
documentCategory: 'passport',
mock: true,
};
const mockSelfApp: SelfApp = {
userId: '0x0000000000000000000000000000000000000000000000000000000000000000',
appName: 'TestSelfApp',
logoBase64: '',
endpointType: 'https',
endpoint: 'https://test.example.com',
deeplinkCallback: '',
header: '',
scope: 'test',
sessionId: '',
userIdType: 'hex',
devMode: false,
disclosures: {},
version: 0,
chainID: 42220,
userDefinedData: '',
};
vi.mock('../../src/stores/protocolStore', () => ({
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`,
);
});
});