Skip to content
8 changes: 8 additions & 0 deletions packages/seedless-onboarding-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add new `SeedlessOnboardingError` class for generic controller errors with support for `cause` and `details` properties ([#7660](https://github.com/MetaMask/core/pull/7660))
- Enables proper error chaining by wrapping underlying errors with additional context
- Includes `toJSON()` method for serialization in logging/transmission

### Changed

- Upgrade `@metamask/utils` from `^11.8.1` to `^11.9.0` ([#7511](https://github.com/MetaMask/core/pull/7511))
- Refactor controller methods to throw `SeedlessOnboardingError` with original error as `cause` for better error tracing ([#7660](https://github.com/MetaMask/core/pull/7660))
- Affected methods: `authenticate`, `changePassword`, `#persistLocalEncryptionKey`, `#fetchAndParseSecretMetadata`, `refreshAuthTokens`

## [7.1.0]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ import {
SeedlessOnboardingControllerErrorMessage,
Web3AuthNetwork,
} from './constants';
import { PasswordSyncError, RecoveryError } from './errors';
import {
PasswordSyncError,
RecoveryError,
SeedlessOnboardingError,
} from './errors';
import { projectLogger, createModuleLogger } from './logger';
import { SecretMetadata } from './SecretMetadata';
import type {
Expand Down Expand Up @@ -437,8 +441,11 @@ export class SeedlessOnboardingController<
return authenticationResult;
} catch (error) {
log('Error authenticating user', error);
throw new Error(
throw new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.AuthenticationError,
{
cause: error,
},
);
}
};
Expand Down Expand Up @@ -664,8 +671,11 @@ export class SeedlessOnboardingController<
);
} catch (error) {
log('Error changing password', error);
throw new Error(
throw new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.FailedToChangePassword,
{
cause: error,
},
);
}
});
Expand Down Expand Up @@ -941,8 +951,11 @@ export class SeedlessOnboardingController<
})
.catch((error) => {
log('Error fetching auth pub key', error);
throw new Error(
throw new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.FailedToFetchAuthPubKey,
{
cause: error,
},
);
});
globalAuthPubKey = authPubKey;
Expand Down Expand Up @@ -1031,8 +1044,11 @@ export class SeedlessOnboardingController<
throw error;
}
log('Error persisting local encryption key', error);
throw new Error(
throw new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.FailedToPersistOprfKey,
{
cause: error,
},
);
}
}
Expand Down Expand Up @@ -1195,8 +1211,11 @@ export class SeedlessOnboardingController<
if (this.#isAuthTokenError(error)) {
throw error;
}
throw new Error(
throw new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.FailedToFetchSecretMetadata,
{
cause: error,
},
);
}

Expand Down Expand Up @@ -1805,8 +1824,11 @@ export class SeedlessOnboardingController<
})
.catch((error) => {
log('Error fetching auth pub key', error);
throw new Error(
throw new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.FailedToFetchAuthPubKey,
{
cause: error,
},
);
});
const isPasswordOutdated = await this.checkIsPasswordOutdated({
Expand Down Expand Up @@ -1844,8 +1866,11 @@ export class SeedlessOnboardingController<
refreshToken,
}).catch((error) => {
log('Error refreshing JWT tokens', error);
throw new Error(
throw new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.FailedToRefreshJWTTokens,
{
cause: error,
},
);
});

Expand All @@ -1866,8 +1891,11 @@ export class SeedlessOnboardingController<
});
} catch (error) {
log('Error refreshing node auth tokens', error);
throw new Error(
throw new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.AuthenticationError,
{
cause: error,
},
);
}
}
Expand Down
207 changes: 206 additions & 1 deletion packages/seedless-onboarding-controller/src/errors.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { TOPRFErrorCode } from '@metamask/toprf-secure-backup';

import { SeedlessOnboardingControllerErrorMessage } from './constants';
import { getErrorMessageFromTOPRFErrorCode } from './errors';
import {
getErrorMessageFromTOPRFErrorCode,
SeedlessOnboardingError,
} from './errors';

describe('getErrorMessageFromTOPRFErrorCode', () => {
it('returns TooManyLoginAttempts for RateLimitExceeded', () => {
Expand Down Expand Up @@ -49,3 +52,205 @@ describe('getErrorMessageFromTOPRFErrorCode', () => {
).toBe('fallback');
});
});

describe('SeedlessOnboardingError', () => {
describe('constructor', () => {
it('creates an error with just a message', () => {
const error = new SeedlessOnboardingError('Test error message');

expect(error.message).toBe('Test error message');
expect(error.name).toBe('SeedlessOnboardingControllerError');
expect(error.details).toBeUndefined();
expect(error.cause).toBeUndefined();
});

it('creates an error with a message from SeedlessOnboardingControllerErrorMessage enum', () => {
const error = new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.AuthenticationError,
);

expect(error.message).toBe(
SeedlessOnboardingControllerErrorMessage.AuthenticationError,
);
expect(error.name).toBe('SeedlessOnboardingControllerError');
});

it('creates an error with message and details', () => {
const error = new SeedlessOnboardingError('Test error', {
details: 'Additional context for debugging',
});

expect(error.message).toBe('Test error');
expect(error.details).toBe('Additional context for debugging');
expect(error.cause).toBeUndefined();
});

it('creates an error with an Error instance as cause', () => {
const originalError = new Error('Original error');
const error = new SeedlessOnboardingError('Wrapped error', {
cause: originalError,
});

expect(error.message).toBe('Wrapped error');
expect(error.cause).toBe(originalError);
});

it('creates an error with a string as cause', () => {
const error = new SeedlessOnboardingError('Test error', {
cause: 'String cause message',
});

expect(error.cause).toBeInstanceOf(Error);
expect(error.cause?.message).toBe('String cause message');
});

it('creates an error with an object as cause (JSON serializable)', () => {
const causeObject = { code: 500, reason: 'Internal error' };
const error = new SeedlessOnboardingError('Test error', {
cause: causeObject,
});

expect(error.cause).toBeInstanceOf(Error);
expect(error.cause?.message).toBe(JSON.stringify(causeObject));
});

it('handles circular object as cause by using fallback message', () => {
const circularObject: Record<string, unknown> = { name: 'circular' };
circularObject.self = circularObject;

const error = new SeedlessOnboardingError('Test error', {
cause: circularObject,
});

expect(error.cause).toBeInstanceOf(Error);
expect(error.cause?.message).toBe('Unknown error');
});

it('creates an error with both details and cause', () => {
const originalError = new Error('Original');
const error = new SeedlessOnboardingError('Test error', {
details: 'Some details',
cause: originalError,
});

expect(error.message).toBe('Test error');
expect(error.details).toBe('Some details');
expect(error.cause).toBe(originalError);
});
});

describe('toJSON', () => {
it('serializes error with all properties', () => {
const originalError = new Error('Original error');
const error = new SeedlessOnboardingError('Test error', {
details: 'Debug info',
cause: originalError,
});

const json = error.toJSON();

expect(json.name).toBe('SeedlessOnboardingControllerError');
expect(json.message).toBe('Test error');
expect(json.details).toBe('Debug info');
expect(json.cause).toStrictEqual({
name: 'Error',
message: 'Original error',
});
expect(json.stack).toBeDefined();
});

it('serializes error without optional properties', () => {
const error = new SeedlessOnboardingError('Simple error');

const json = error.toJSON();

expect(json.name).toBe('SeedlessOnboardingControllerError');
expect(json.message).toBe('Simple error');
expect(json.details).toBeUndefined();
expect(json.cause).toBeUndefined();
expect(json.stack).toBeDefined();
});

it('serializes error with custom error type as cause', () => {
class CustomError extends Error {
constructor() {
super('Custom error message');
this.name = 'CustomError';
}
}
const customError = new CustomError();
const error = new SeedlessOnboardingError('Wrapper', {
cause: customError,
});

const json = error.toJSON();

expect(json.cause).toStrictEqual({
name: 'CustomError',
message: 'Custom error message',
});
});

it('serializes SeedlessOnboardingError cause with details preserved', () => {
const innerError = new SeedlessOnboardingError('Inner error', {
details: 'Inner debugging context',
});
const outerError = new SeedlessOnboardingError('Outer error', {
details: 'Outer debugging context',
cause: innerError,
});

const json = outerError.toJSON();

expect(json.name).toBe('SeedlessOnboardingControllerError');
expect(json.message).toBe('Outer error');
expect(json.details).toBe('Outer debugging context');
expect(json.cause).toStrictEqual({
name: 'SeedlessOnboardingControllerError',
message: 'Inner error',
details: 'Inner debugging context',
cause: undefined,
stack: innerError.stack,
});
});

it('serializes deeply nested SeedlessOnboardingError chain', () => {
const rootError = new Error('Root cause');
const level1 = new SeedlessOnboardingError('Level 1', {
details: 'Level 1 details',
cause: rootError,
});
const level2 = new SeedlessOnboardingError('Level 2', {
details: 'Level 2 details',
cause: level1,
});

const json = level2.toJSON();

expect(json.details).toBe('Level 2 details');
const level1Json = json.cause as Record<string, unknown>;
expect(level1Json.message).toBe('Level 1');
expect(level1Json.details).toBe('Level 1 details');
expect(level1Json.cause).toStrictEqual({
name: 'Error',
message: 'Root cause',
});
});
});

describe('inheritance', () => {
it('is an instance of Error', () => {
const error = new SeedlessOnboardingError('Test');

expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(SeedlessOnboardingError);
});

it('has a proper stack trace', () => {
const error = new SeedlessOnboardingError('Test');

expect(error.stack).toBeDefined();
expect(error.stack).toContain('SeedlessOnboardingControllerError');
});
});
});
Loading
Loading