Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions packages/mobile-sdk-alpha/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,13 @@ export type {
MRZValidation,
NetworkAdapter,
Progress,
ProofHandle,
ProofRequest,
RegistrationInput,
RegistrationStatus,
ScanMode,
ScanOpts,
ScanResult,
ScannerAdapter,
SelfClient,
StorageAdapter,
Unsubscribe,
ValidationInput,
ValidationResult,
WsAdapter,
WsConn,
} from './types/public';
Expand Down
68 changes: 10 additions & 58 deletions packages/mobile-sdk-alpha/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,15 @@ import { defaultConfig } from './config/defaults';
import { mergeConfig } from './config/merge';
import { notImplemented } from './errors';
import { extractMRZInfo as parseMRZInfo } from './processing/mrz';
import { SDKEvent, SDKEventMap, SdkEvents } from './types/events';
import type {
Adapters,
Config,
Progress,
ProofHandle,
ProofRequest,
RegistrationInput,
RegistrationStatus,
ScanOpts,
ScanResult,
SelfClient,
Unsubscribe,
ValidationInput,
ValidationResult,
} from './types/public';
import { SDKEvent, SDKEventMap } from './types/events';
import type { Adapters, Config, ScanOpts, ScanResult, SelfClient, Unsubscribe } from './types/public';
import { TrackEventParams } from './types/public';
/**
* Optional adapter implementations used when a consumer does not provide their
* own. These defaults are intentionally minimal no-ops suitable for tests and
* non-production environments.
*/
const optionalDefaults: Required<Pick<Adapters, 'storage' | 'clock' | 'logger'>> = {
storage: {
get: async () => null,
set: async () => {},
remove: async () => {},
},
const optionalDefaults: Required<Pick<Adapters, 'clock' | 'logger'>> = {
clock: {
now: () => Date.now(),
sleep: async (ms: number) => {
Expand Down Expand Up @@ -115,39 +96,14 @@ export function createSelfClient({
}

async function scanDocument(opts: ScanOpts & { signal?: AbortSignal }): Promise<ScanResult> {
return _adapters.scanner.scan(opts);
}

async function validateDocument(_input: ValidationInput): Promise<ValidationResult> {
return { ok: false, reason: 'SELF_ERR_VALIDATION_STUB' };
}

async function checkRegistration(_input: RegistrationInput): Promise<RegistrationStatus> {
return { registered: false, reason: 'SELF_REG_STATUS_STUB' };
}

async function registerDocument(_input: RegistrationInput): Promise<RegistrationStatus> {
return { registered: false, reason: 'SELF_REG_STATUS_STUB' };
}
// Apply scanner timeout from config if no signal provided
if (!opts.signal && cfg.timeouts.scanMs) {
const controller = new AbortController();
setTimeout(() => controller.abort(), cfg.timeouts.scanMs);
return _adapters.scanner.scan({ ...opts, signal: controller.signal });
}

async function generateProof(
_req: ProofRequest,
opts: {
signal?: AbortSignal;
onProgress?: (p: Progress) => void;
timeoutMs?: number;
} = {},
): Promise<ProofHandle> {
if (!adapters.network) throw notImplemented('network');
if (!adapters.crypto) throw notImplemented('crypto');
const timeoutMs = opts.timeoutMs ?? cfg.timeouts?.proofMs ?? defaultConfig.timeouts.proofMs;
void _adapters.clock.sleep(timeoutMs!, opts.signal).then(() => emit(SdkEvents.ERROR, new Error('timeout')));
return {
id: 'stub',
status: 'pending',
result: async () => ({ ok: false, reason: 'SELF_ERR_PROOF_STUB' }),
cancel: () => {},
};
return _adapters.scanner.scan(opts);
Comment on lines +99 to +106
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Prevent leaked timers and late aborts in scanDocument

The timeout-created AbortController is never cleaned up. If scan resolves fast, the pending timer still fires and aborts after completion, which can cause adapter-side races and needless wakeups. Wrap with try/finally and clear the timer.

Apply this diff:

-  // Apply scanner timeout from config if no signal provided
-  if (!opts.signal && cfg.timeouts.scanMs) {
-    const controller = new AbortController();
-    setTimeout(() => controller.abort(), cfg.timeouts.scanMs);
-    return _adapters.scanner.scan({ ...opts, signal: controller.signal });
-  }
-
-  return _adapters.scanner.scan(opts);
+  // Apply scanner timeout from config if no signal provided
+  if (!opts.signal && cfg.timeouts.scanMs) {
+    const controller = new AbortController();
+    const timer = setTimeout(() => controller.abort(), cfg.timeouts.scanMs);
+    try {
+      return await _adapters.scanner.scan({ ...opts, signal: controller.signal });
+    } finally {
+      clearTimeout(timer);
+    }
+  }
+
+  return _adapters.scanner.scan(opts);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Apply scanner timeout from config if no signal provided
if (!opts.signal && cfg.timeouts.scanMs) {
const controller = new AbortController();
setTimeout(() => controller.abort(), cfg.timeouts.scanMs);
return _adapters.scanner.scan({ ...opts, signal: controller.signal });
}
async function generateProof(
_req: ProofRequest,
opts: {
signal?: AbortSignal;
onProgress?: (p: Progress) => void;
timeoutMs?: number;
} = {},
): Promise<ProofHandle> {
if (!adapters.network) throw notImplemented('network');
if (!adapters.crypto) throw notImplemented('crypto');
const timeoutMs = opts.timeoutMs ?? cfg.timeouts?.proofMs ?? defaultConfig.timeouts.proofMs;
void _adapters.clock.sleep(timeoutMs!, opts.signal).then(() => emit(SdkEvents.ERROR, new Error('timeout')));
return {
id: 'stub',
status: 'pending',
result: async () => ({ ok: false, reason: 'SELF_ERR_PROOF_STUB' }),
cancel: () => {},
};
return _adapters.scanner.scan(opts);
// Apply scanner timeout from config if no signal provided
if (!opts.signal && cfg.timeouts.scanMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), cfg.timeouts.scanMs);
try {
return await _adapters.scanner.scan({ ...opts, signal: controller.signal });
} finally {
clearTimeout(timer);
}
}
return _adapters.scanner.scan(opts);
🤖 Prompt for AI Agents
In packages/mobile-sdk-alpha/src/client.ts around lines 100 to 107, the
AbortController timer created for scan timeout is never cleared which can fire
after scan resolves; change the logic to create the controller and timer, call
the scanner with await (no early return), and wrap the await in try/finally
where finally calls clearTimeout(timerId) so the timer is cancelled if scan
completes early (leave the abort() only in the timer handler so it only runs if
not cleared).

}

async function trackEvent(event: string, payload?: TrackEventParams): Promise<void> {
Expand Down Expand Up @@ -177,13 +133,9 @@ export function createSelfClient({

return {
scanDocument,
validateDocument,
trackEvent,
getPrivateKey,
hasPrivateKey,
checkRegistration,
registerDocument,
generateProof,
extractMRZInfo: parseMRZInfo,
on,
emit,
Expand Down
5 changes: 2 additions & 3 deletions packages/mobile-sdk-alpha/src/config/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
import type { Config } from '../types/public';

export const defaultConfig: Required<Config> = {
endpoints: { api: '', teeWs: '', artifactsCdn: '' },
timeouts: { httpMs: 30000, wsMs: 60000, scanMs: 60000, proofMs: 120000 },
timeouts: { scanMs: 60000 },
// in future this can be used to enable/disable experimental features
features: {},
tlsPinning: { enabled: false },
};
2 changes: 0 additions & 2 deletions packages/mobile-sdk-alpha/src/config/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ export function mergeConfig(base: Required<Config>, override: Config): Required<
return {
...base,
...override,
endpoints: { ...base.endpoints, ...(override.endpoints ?? {}) },
timeouts: { ...base.timeouts, ...(override.timeouts ?? {}) },
features: { ...base.features, ...(override.features ?? {}) },
tlsPinning: { ...base.tlsPinning, ...(override.tlsPinning ?? {}) },
};
}
6 changes: 0 additions & 6 deletions packages/mobile-sdk-alpha/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ export type {
MRZValidation,
NetworkAdapter,
Progress,
ProofHandle,
ProofRequest,
RegistrationInput,
RegistrationStatus,
ScanMode,
ScanOpts,
ScanResult,
Expand All @@ -30,8 +26,6 @@ export type {
StorageAdapter,
TrackEventParams,
Unsubscribe,
ValidationInput,
ValidationResult,
WsAdapter,
WsConn,
} from './types/public';
Expand Down
54 changes: 52 additions & 2 deletions packages/mobile-sdk-alpha/src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,65 @@ import { DocumentCategory } from '@selfxyz/common/types';
import type { Progress } from './public';

export enum SdkEvents {
/**
* Emitted when an error occurs during SDK operations, including timeouts.
*
* **Required:** Handle this event to provide error feedback to users.
* **Recommended:** Log errors for debugging and show appropriate user-friendly error messages.
*/
ERROR = 'ERROR',

/**
* Emitted to provide progress updates during long-running operations.
*
* **Recommended:** Use this to show progress indicators or loading states to improve user experience.
*/
PROGRESS = 'PROGRESS',
STATE = 'STATE',

/**
* Emitted when no passport data is found on the device during initialization.
*
* **Required:** Navigate users to a document scanning/setup screen to capture their passport data.
* **Recommended:** Provide clear instructions on how to scan and register their document properly.
*/
PROVING_PASSPORT_DATA_NOT_FOUND = 'PROVING_PASSPORT_DATA_NOT_FOUND',

/**
* Emitted when identity verification completes successfully.
*
* **Required:** Show success confirmation to the user that their identity was verified.
* **Recommended:** Navigate to your app's main screen or success page after a brief delay (3 seconds)
* to allow users to see the success state.
*/
PROVING_ACCOUNT_VERIFIED_SUCCESS = 'PROVING_ACCOUNT_VERIFIED_SUCCESS',

/**
* Emitted when document registration fails or encounters an error.
*
* **Required:** Handle navigation based on the `hasValidDocument` parameter:
* - If `hasValidDocument` is `true`: Navigate to your app's home screen (user has other valid documents registered)
* - If `hasValidDocument` is `false`: Navigate to launch/onboarding screen (user needs to register documents)
* **Recommended:** Show appropriate error messages and implement a brief delay (3 seconds) before navigation.
*/
PROVING_REGISTER_ERROR_OR_FAILURE = 'PROVING_REGISTER_ERROR_OR_FAILURE',

/**
* Emitted when a passport from an unsupported country or document type is detected during validation.
*
* **Required:** Inform users that their document is not currently supported.
* **Recommended:** Navigate to an unsupported document screen showing the detected country code and
* document category, and provide guidance on alternative verification methods if available.
*/
PROVING_PASSPORT_NOT_SUPPORTED = 'PROVING_PASSPORT_NOT_SUPPORTED',

/**
* Emitted when account recovery is required because the passport was registered with different credentials.
* This happens when a document's nullifier is found on-chain but not registered with the current user's secret.
*
* **Required:** Navigate users to an account recovery screen with recovery options or instructions if the have originally registered with a differnt self app.
* **Recommended:** Explain that their passport was previously registered with different account credentials
* and guide them through the recovery process to regain access.
*/
PROVING_ACCOUNT_RECOVERY_REQUIRED = 'PROVING_ACCOUNT_RECOVERY_REQUIRED',
}

Expand All @@ -31,7 +82,6 @@ export interface SDKEventMap {
[SdkEvents.PROVING_ACCOUNT_RECOVERY_REQUIRED]: undefined;

[SdkEvents.PROGRESS]: Progress;
[SdkEvents.STATE]: string;
[SdkEvents.ERROR]: Error;
}

Expand Down
43 changes: 0 additions & 43 deletions packages/mobile-sdk-alpha/src/types/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,10 @@ import { SDKEvent, SDKEventMap } from './events';
export type { PassportValidationCallbacks } from '../validation/document';
export type { DocumentCatalog, PassportData };
export interface Config {
endpoints?: { api?: string; teeWs?: string; artifactsCdn?: string };
timeouts?: {
httpMs?: number;
wsMs?: number;
scanMs?: number;
proofMs?: number;
};
features?: Record<string, boolean>;
tlsPinning?: { enabled: boolean; pins?: string[] };
}
export interface CryptoAdapter {
hash(input: Uint8Array, algo?: 'sha256'): Promise<Uint8Array>;
Expand Down Expand Up @@ -107,35 +102,15 @@ export interface Adapters {
documents: DocumentsAdapter;
}

export interface ProofHandle {
id: string;
status: 'pending' | 'completed' | 'failed';
result: () => Promise<{ ok: boolean; reason?: string }>;
cancel: () => void;
}
export interface LoggerAdapter {
log(level: LogLevel, message: string, fields?: Record<string, unknown>): void;
}

export interface ProofRequest {
type: 'register' | 'dsc' | 'disclose';
payload: unknown;
}
export interface NetworkAdapter {
http: HttpAdapter;
ws: WsAdapter;
}

export interface RegistrationInput {
docId?: string;
scan: ScanResult;
}

export interface RegistrationStatus {
registered: boolean;
reason?: string;
}

export type ScanMode = 'mrz' | 'nfc' | 'qr';

export type ScanOpts =
Expand Down Expand Up @@ -186,17 +161,6 @@ export interface DocumentsAdapter {

export interface SelfClient {
scanDocument(opts: ScanOpts & { signal?: AbortSignal }): Promise<ScanResult>;
validateDocument(input: ValidationInput): Promise<ValidationResult>;
checkRegistration(input: RegistrationInput): Promise<RegistrationStatus>;
registerDocument(input: RegistrationInput): Promise<RegistrationStatus>;
generateProof(
req: ProofRequest,
opts?: {
signal?: AbortSignal;
onProgress?: (p: Progress) => void;
timeoutMs?: number;
},
): Promise<ProofHandle>;
extractMRZInfo(mrz: string): MRZInfo;
trackEvent(event: string, payload?: TrackEventParams): void;
getPrivateKey(): Promise<string | null>;
Expand All @@ -218,13 +182,6 @@ export interface StorageAdapter {
set(key: string, value: string): Promise<void>;
remove(key: string): Promise<void>;
}
export interface ValidationInput {
scan: ScanResult;
}
export interface ValidationResult {
ok: boolean;
reason?: string;
}
export interface WsAdapter {
connect(url: string, opts?: { signal?: AbortSignal; headers?: Record<string, string> }): WsConn;
}
Expand Down
2 changes: 0 additions & 2 deletions packages/mobile-sdk-alpha/tests/client-mrz.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ describe('createSelfClient API', () => {
const client = createSelfClient({ config: {}, adapters: mockAdapters, listeners: new Map() });

expect(typeof client.extractMRZInfo).toBe('function');
expect(typeof client.registerDocument).toBe('function');
expect(typeof client.validateDocument).toBe('function');
});

it('parses MRZ data correctly', () => {
Expand Down
34 changes: 6 additions & 28 deletions packages/mobile-sdk-alpha/tests/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,12 @@ describe('createSelfClient', () => {
});
const result = await client.scanDocument({ mode: 'qr' });
expect(result).toEqual({ mode: 'qr', data: 'self://ok' });
expect(scanMock).toHaveBeenCalledWith({ mode: 'qr' });
expect(scanMock).toHaveBeenCalledWith(
expect.objectContaining({
mode: 'qr',
signal: expect.any(AbortSignal),
}),
);
});

it('propagates scanner errors', async () => {
Expand All @@ -78,22 +83,6 @@ describe('createSelfClient', () => {
await expect(client.scanDocument({ mode: 'qr' })).rejects.toBe(err);
});

it('returns stub proof handle when adapters provided', async () => {
const network = { http: { fetch: vi.fn() }, ws: { connect: vi.fn() } } as any;
const crypto = { hash: vi.fn(), sign: vi.fn() } as any;
const scanner = { scan: vi.fn() } as any;
const client = createSelfClient({
config: {},
adapters: { network, crypto, scanner, documents, auth },
listeners: new Map(),
});
const handle = await client.generateProof({ type: 'register', payload: {} });
expect(handle.id).toBe('stub');
expect(handle.status).toBe('pending');
expect(await handle.result()).toEqual({ ok: false, reason: 'SELF_ERR_PROOF_STUB' });
expect(() => handle.cancel()).not.toThrow();
});

it('emits and unsubscribes events', () => {
const listeners = createListenersMap();

Expand Down Expand Up @@ -144,17 +133,6 @@ describe('createSelfClient', () => {
expect(info.validation?.overall).toBe(true);
});

it('returns stub registration status', async () => {
const client = createSelfClient({
config: {},
adapters: { scanner, network, crypto, documents, auth },
listeners: new Map(),
});
await expect(client.registerDocument({} as any)).resolves.toEqual({
registered: false,
reason: 'SELF_REG_STATUS_STUB',
});
});
describe('when analytics adapter is given', () => {
it('calls that adapter for trackEvent', () => {
const trackEvent = vi.fn();
Expand Down
Loading
Loading