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
10 changes: 10 additions & 0 deletions packages/mobile-sdk-alpha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ const compact = formatDateToYYMMDD('1974-08-12');
const nfc = parseNFCResponse(rawBytes);
```

## Error handling

The SDK surfaces typed errors for clearer diagnostics:

- `NfcParseError` and `MrzParseError` for NFC and MRZ parsing issues (category `validation`)
- `InitError` for initialization problems (category `init`)
- `LivenessError` for liveness failures (category `liveness`)

All errors extend `SdkError`, which includes a `code`, `category`, and `retryable` flag.

## Migration checklist

Track progress in [MIGRATION_CHECKLIST.md](./docs/MIGRATION_CHECKLIST.md).
Expand Down
8 changes: 4 additions & 4 deletions packages/mobile-sdk-alpha/docs/ARCHITECTURE_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ The alpha SDK follows an adapter-first, React Native–oriented design. Tree-sha
- [x] Create unified event handling interface
- [x] Implement platform-specific event bridges

### 3. Exception classes
### 3. Exception classes

- [ ] Add typed errors (`InitError`, `LivenessError`, etc.)
- [ ] Surface typed errors instead of generic `Error`
- [ ] Ensure consistent error categorization
- [x] Add typed errors (`InitError`, `LivenessError`, `NfcParseError`, `MrzParseError`)
- [x] Surface typed errors instead of generic `Error`
- [x] Ensure consistent error categorization

### 4. SDK lifecycle management

Expand Down
11 changes: 11 additions & 0 deletions packages/mobile-sdk-alpha/docs/errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Error Classes

| Error Class | Category | Code | Description |
| --------------- | ------------ | -------------------- | ------------------------------------------------------------ |
| `NfcParseError` | `validation` | `SELF_ERR_NFC_PARSE` | Thrown when NFC byte streams are malformed or decoding fails |
| `MrzParseError` | `validation` | `SELF_ERR_MRZ_PARSE` | Raised for invalid MRZ characters or formats |
| `InitError` | `init` | `SELF_ERR_INIT` | Issues during SDK initialization |
| `LivenessError` | `liveness` | `SELF_ERR_LIVENESS` | Errors from liveness checks |
| `SdkError` | varies | custom | Base class for all SDK errors |

Use typed errors instead of generic `Error` to surface clearer failure modes and consistent categorization.
14 changes: 14 additions & 0 deletions packages/mobile-sdk-alpha/src/errors/InitError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SdkError } from './SdkError';

/**
* Error thrown when the SDK fails to initialize correctly.
*
* @param message - description of the initialization failure.
* @param options - optional underlying error details.
*/
export class InitError extends SdkError {
constructor(message: string, options?: { cause?: unknown }) {
super(message, 'SELF_ERR_INIT', 'init', false, options);
this.name = 'InitError';
}
}
14 changes: 14 additions & 0 deletions packages/mobile-sdk-alpha/src/errors/LivenessError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SdkError } from './SdkError';

/**
* Error thrown when liveness checks detect an issue.
*
* @param message - description of the liveness failure.
* @param options - optional underlying error details.
*/
export class LivenessError extends SdkError {
constructor(message: string, options?: { cause?: unknown }) {
super(message, 'SELF_ERR_LIVENESS', 'liveness', false, options);
this.name = 'LivenessError';
}
}
14 changes: 14 additions & 0 deletions packages/mobile-sdk-alpha/src/errors/MrzParseError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SdkError } from './SdkError';

/**
* Error thrown when an MRZ string fails validation or parsing.
*
* @param message - description of the MRZ parsing failure.
* @param options - optional underlying error details.
*/
export class MrzParseError extends SdkError {
constructor(message: string, options?: { cause?: unknown }) {
super(message, 'SELF_ERR_MRZ_PARSE', 'validation', false, options);
this.name = 'MrzParseError';
}
}
14 changes: 14 additions & 0 deletions packages/mobile-sdk-alpha/src/errors/NfcParseError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SdkError } from './SdkError';

/**
* Error thrown when NFC data cannot be parsed.
*
* @param message - description of the parsing failure.
* @param options - optional underlying error details.
*/
export class NfcParseError extends SdkError {
constructor(message: string, options?: { cause?: unknown }) {
super(message, 'SELF_ERR_NFC_PARSE', 'validation', false, options);
this.name = 'NfcParseError';
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
// Add ErrorOptions interface for TypeScript compatibility
interface ErrorOptions {
cause?: unknown;
}

export type SdkErrorCategory = 'scanner' | 'network' | 'protocol' | 'proof' | 'crypto' | 'validation' | 'config';

export const SCANNER_ERROR_CODES = {
UNAVAILABLE: 'SELF_ERR_SCANNER_UNAVAILABLE',
NFC_NOT_SUPPORTED: 'SELF_ERR_NFC_NOT_SUPPORTED',
INVALID_MODE: 'SELF_ERR_SCANNER_MODE',
} as const;
export type SdkErrorCategory =
| 'scanner'
| 'network'
| 'protocol'
| 'proof'
| 'crypto'
| 'validation'
| 'config'
| 'init'
| 'liveness';

/**
* Base class for all SDK errors.
*/
export class SdkError extends Error {
readonly code: string;
readonly category: SdkErrorCategory;
Expand All @@ -29,10 +34,25 @@ export class SdkError extends Error {
}
}

/**
* Helper to create an SDK error for an adapter that has not been provided.
*
* @param name - human-readable adapter name.
* @returns configured {@link SdkError} instance.
*/
export function notImplemented(name: string) {
return new SdkError(`${name} adapter not provided`, 'SELF_ERR_ADAPTER_MISSING', 'config', false);
}

/**
* Convenience factory for {@link SdkError}.
*
* @param message - error description.
* @param code - unique error code.
* @param category - high level error category.
* @param retryable - whether the operation may be retried.
* @returns configured {@link SdkError} instance.
*/
export function sdkError(message: string, code: string, category: SdkErrorCategory, retryable = false) {
return new SdkError(message, code, category, retryable);
}
12 changes: 12 additions & 0 deletions packages/mobile-sdk-alpha/src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export { InitError } from './InitError';

export { LivenessError } from './LivenessError';
export { MrzParseError } from './MrzParseError';
export { NfcParseError } from './NfcParseError';
export const SCANNER_ERROR_CODES = {
UNAVAILABLE: 'SELF_ERR_SCANNER_UNAVAILABLE',
NFC_NOT_SUPPORTED: 'SELF_ERR_NFC_NOT_SUPPORTED',
INVALID_MODE: 'SELF_ERR_SCANNER_MODE',
} as const;

export { SdkError, type SdkErrorCategory, notImplemented, sdkError } from './SdkError';
15 changes: 12 additions & 3 deletions packages/mobile-sdk-alpha/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,19 @@ export type { MRZScanOptions } from './mrz';
export type { PassportValidationCallbacks } from './validation/document';

export type { QRProofOptions } from './qr';
// NFC module
export type { SdkErrorCategory } from './errors';

export { SCANNER_ERROR_CODES, notImplemented, sdkError } from './errors';
// Error handling
export type { SdkErrorCategory } from './errors';
export {
InitError,
LivenessError,
MrzParseError,
NfcParseError,
SCANNER_ERROR_CODES,
SdkError,
notImplemented,
sdkError,
} from './errors';

export { createSelfClient } from './client';

Expand Down
11 changes: 6 additions & 5 deletions packages/mobile-sdk-alpha/src/processing/mrz.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MrzParseError } from '../errors';
import type { MRZInfo, MRZValidation } from '../types/public';

/**
Expand All @@ -18,7 +19,7 @@ function calculateCheckDigit(input: string): number {
} else if (char === '<') {
value = 0;
} else {
throw new Error(`Invalid character in MRZ: ${char}`);
throw new MrzParseError(`Invalid character in MRZ: ${char}`);
}

sum += value * weights[i % 3];
Expand Down Expand Up @@ -204,7 +205,7 @@ function validateTD3CheckDigits(lines: string[]): Omit<MRZValidation, 'format' |
*/
export function extractMRZInfo(mrzString: string): MRZInfo {
if (!mrzString || typeof mrzString !== 'string') {
throw new Error('MRZ string is required and must be a string');
throw new MrzParseError('MRZ string is required and must be a string');
}

const lines = mrzString
Expand All @@ -216,7 +217,7 @@ export function extractMRZInfo(mrzString: string): MRZInfo {
const isValidTD3 = validateTD3Format(lines);

if (!isValidTD3) {
throw new Error(
throw new MrzParseError(
`Invalid MRZ format: Expected TD3 format (2 lines × 44 characters), got ${lines.length} lines with lengths [${lines.map(l => l.length).join(', ')}]`,
);
}
Expand Down Expand Up @@ -246,7 +247,7 @@ export function extractMRZInfo(mrzString: string): MRZInfo {
*/
export function formatDateToYYMMDD(inputDate: string): string {
if (!inputDate || typeof inputDate !== 'string') {
throw new Error('Date string is required');
throw new MrzParseError('Date string is required');
}

// Handle ISO date strings (YYYY-MM-DD format)
Expand All @@ -271,5 +272,5 @@ export function formatDateToYYMMDD(inputDate: string): string {
return year.slice(2) + month + day;
}

throw new Error(`Invalid date format: ${inputDate}. Expected ISO format (YYYY-MM-DD) or similar.`);
throw new MrzParseError(`Invalid date format: ${inputDate}. Expected ISO format (YYYY-MM-DD) or similar.`);
}
14 changes: 8 additions & 6 deletions packages/mobile-sdk-alpha/src/processing/nfc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Safe TextDecoder factory that works across different JavaScript environments.
* Handles browser, Node.js, and React Native environments gracefully.
*/
import { NfcParseError } from '../errors';

const createTextDecoder = (): TextDecoder => {
// Browser environment - TextDecoder is available globally
if (typeof globalThis !== 'undefined' && 'TextDecoder' in globalThis) {
Expand Down Expand Up @@ -31,7 +33,7 @@ const createTextDecoder = (): TextDecoder => {
}
}

throw new Error(
throw new NfcParseError(
'TextDecoder not available in this environment. ' +
'This SDK requires TextDecoder support which is available in modern browsers, Node.js, and React Native.',
);
Expand Down Expand Up @@ -64,16 +66,16 @@ export interface ParsedNFCResponse {

function readLength(view: Uint8Array, offset: number): { length: number; next: number } {
if (offset >= view.length) {
throw new Error('Unexpected end of data while reading length');
throw new NfcParseError('Unexpected end of data while reading length');
}
const first = view[offset];
if (first & 0x80) {
const bytes = first & 0x7f;
if (bytes === 0) {
throw new Error('Indefinite length (0x80) not supported');
throw new NfcParseError('Indefinite length (0x80) not supported');
}
if (offset + bytes >= view.length) {
throw new Error('Unexpected end of data while reading long-form length');
throw new NfcParseError('Unexpected end of data while reading long-form length');
}
let len = 0;
for (let j = 1; j <= bytes; j++) {
Expand All @@ -92,10 +94,10 @@ export function parseNFCResponse(bytes: Uint8Array): ParsedNFCResponse {
let i = 0;
while (i < bytes.length) {
const tag = bytes[i++];
if (i >= bytes.length) throw new Error('Unexpected end of data');
if (i >= bytes.length) throw new NfcParseError('Unexpected end of data');
const { length, next } = readLength(bytes, i);
i = next;
if (i + length > bytes.length) throw new Error('Unexpected end of data');
if (i + length > bytes.length) throw new NfcParseError('Unexpected end of data');
const value = bytes.slice(i, i + length);
i += length;

Expand Down
47 changes: 47 additions & 0 deletions packages/mobile-sdk-alpha/tests/errors.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';

import { InitError, LivenessError, MrzParseError, NfcParseError } from '../src';
import { notImplemented, SdkError, sdkError } from '../src/errors';

describe('SdkError', () => {
Expand Down Expand Up @@ -75,3 +76,49 @@ describe('notImplemented factory function', () => {
expect(error.retryable).toBe(false);
});
});

describe('Specific error classes', () => {
it('can instantiate InitError', () => {
const error = new InitError('Initialization failed');

expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(SdkError);
expect(error).toBeInstanceOf(InitError);
expect(error.name).toBe('InitError');
expect(error.message).toBe('Initialization failed');
expect(error.category).toBe('init');
});

it('can instantiate LivenessError', () => {
const error = new LivenessError('Liveness check failed');

expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(SdkError);
expect(error).toBeInstanceOf(LivenessError);
expect(error.name).toBe('LivenessError');
expect(error.message).toBe('Liveness check failed');
expect(error.category).toBe('liveness');
});

it('can instantiate MrzParseError', () => {
const error = new MrzParseError('MRZ parsing failed');

expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(SdkError);
expect(error).toBeInstanceOf(MrzParseError);
expect(error.name).toBe('MrzParseError');
expect(error.message).toBe('MRZ parsing failed');
expect(error.category).toBe('validation');
});

it('can instantiate NfcParseError', () => {
const error = new NfcParseError('NFC parsing failed');

expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(SdkError);
expect(error).toBeInstanceOf(NfcParseError);
expect(error.name).toBe('NfcParseError');
expect(error.message).toBe('NFC parsing failed');
expect(error.category).toBe('validation');
});
});
6 changes: 3 additions & 3 deletions packages/mobile-sdk-alpha/tests/processing/mrz.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';

import { extractMRZInfo, formatDateToYYMMDD } from '../../src';
import { extractMRZInfo, formatDateToYYMMDD, MrzParseError } from '../../src';

const sample = `P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<
L898902C36UTO7408122F1204159ZE184226B<<<<<10`;
Expand All @@ -14,7 +14,7 @@ describe('extractMRZInfo', () => {

it('rejects malformed MRZ', () => {
const invalid = 'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<';
expect(() => extractMRZInfo(invalid)).toThrow();
expect(() => extractMRZInfo(invalid)).toThrowError(MrzParseError);
});

it('flags bad check digits', () => {
Expand All @@ -35,6 +35,6 @@ describe('formatDateToYYMMDD', () => {
});

it('throws on invalid input', () => {
expect(() => formatDateToYYMMDD('invalid')).toThrow();
expect(() => formatDateToYYMMDD('invalid')).toThrowError(MrzParseError);
});
});
4 changes: 2 additions & 2 deletions packages/mobile-sdk-alpha/tests/processing/nfc.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';

import { parseNFCResponse } from '../../src';
import { NfcParseError, parseNFCResponse } from '../../src';

const enc = new TextEncoder();
const mrz = `P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<
Expand All @@ -24,7 +24,7 @@ describe('parseNFCResponse', () => {

it('throws on truncated data', () => {
const bad = new Uint8Array([0x61, 0x05, 0x01]);
expect(() => parseNFCResponse(bad)).toThrow('Unexpected end of data');
expect(() => parseNFCResponse(bad)).toThrowError(NfcParseError);
});

it('ignores unknown tags', () => {
Expand Down
Loading
Loading