Skip to content

Commit

Permalink
PSG-3862: improve error handling (#15)
Browse files Browse the repository at this point in the history
* refactor: removes transaction method parameter wrapper types for the generated types

* fix: now passes the error info from the api response through to the PassageError object

* Change files

* refactor: renames PassageError fields to be more aligned with the client sdks

* chore: wraps generated error code exports and sets them as a string literal union for PassageError.statusText
  • Loading branch information
ctran88 authored Apr 23, 2024
1 parent fdfe748 commit 9854556
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 82 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "fix: now passes the error info from the api response through to the PassageError object",
"packageName": "@passageidentity/passage-flex-node",
"email": "[email protected]",
"dependentChangeType": "patch"
}
73 changes: 58 additions & 15 deletions src/classes/PassageError.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,70 @@
import { ResponseError } from '../generated';
import {
Model400ErrorCodeEnum,
Model401ErrorCodeEnum,
Model403ErrorCodeEnum,
Model404ErrorCodeEnum,
Model409ErrorCodeEnum,
Model500ErrorCodeEnum,
ResponseError,
} from '../generated';

export const ErrorStatusText = {
...Model400ErrorCodeEnum,
...Model401ErrorCodeEnum,
...Model403ErrorCodeEnum,
...Model404ErrorCodeEnum,
...Model409ErrorCodeEnum,
...Model500ErrorCodeEnum,
};

type ErrorStatusText = (typeof ErrorStatusText)[keyof typeof ErrorStatusText];
type APIResponseError = { statusCode: number; statusText: ErrorStatusText; errorMessage: string };

/**
* PassageError Class used to handle errors from PassageFlex
*/
export class PassageError extends Error {
readonly statusCode: number | undefined;
readonly error: string | undefined;
readonly message: string;
public readonly statusCode: number | undefined;
public readonly statusText: ErrorStatusText | undefined;

/**
* Initialize a new PassageError instance.
* @param {string} message friendly message,
* @param {Error} err error from node-fetch request
* @param {string} message friendly message
* @param {APIResponseError} response error information from PassageFlex API
*/
constructor(message: string, err?: ResponseError) {
super();

if (err) {
this.message = `${message}: ${err.message}`;
this.statusCode = 500;
this.error = err.message;
} else {
this.message = message;
private constructor(message: string, response?: APIResponseError) {
super(message);

if (!response) {
return;
}

this.message = `${message}: ${response.errorMessage}`;
this.statusCode = response.statusCode;
this.statusText = response.statusText;
}

/**
* Initialize a new PassageError instance.
* @param {string} message friendly message
* @return {PassageError}
*/
public static fromMessage(message: string): PassageError {
return new PassageError(message);
}

/**
* Initialize a new PassageError instance.
* @param {string} message friendly message
* @param {ResponseError} err error from node-fetch request
* @return {Promise<PassageError>}
*/
public static async fromResponseError(message: string, err: ResponseError): Promise<PassageError> {
const body: { code: ErrorStatusText; error: string } = await err.response.json();
return new PassageError(message, {
statusCode: err.response.status,
statusText: body.code,
errorMessage: body.error,
});
}
}
69 changes: 45 additions & 24 deletions src/classes/PassageFlex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
AppsApi,
AuthenticateApi,
Configuration,
CreateTransactionAuthenticateRequest,
CreateTransactionRegisterRequest,
ResponseError,
TransactionsApi,
UserDevicesApi,
Expand All @@ -12,9 +14,7 @@ import {
} from '../generated';
import apiConfiguration from '../utils/apiConfiguration';
import { AppInfo } from '../models/AppInfo';
import { AuthenticateTransactionArgs } from '../types/AuthenticateTransactionArgs';
import { UserInfo } from '../models/UserInfo';
import { RegisterTransactionArgs } from '../types/RegisterTransactionArgs';

/**
* PassageFlex class used to get app info, create transactions, and verify nonces
Expand All @@ -35,7 +35,7 @@ export class PassageFlex {
*/
public constructor(config: PassageConfig) {
if (!config.appId || !config.apiKey) {
throw new PassageError(
throw PassageError.fromMessage(
'A Passage appId and apiKey are required. Please include {appId: YOUR_APP_ID, apiKey: YOUR_APP_ID}.',
);
}
Expand Down Expand Up @@ -73,52 +73,57 @@ export class PassageFlex {

return appInfo;
} catch (err) {
throw new PassageError('Could not fetch app', err as ResponseError);
if (err instanceof ResponseError) {
throw await PassageError.fromResponseError('Could not fetch app', err);
}

throw err;
}
}

/**
* Create a transaction to start a user's registration process
*
* @param {RegisterTransactionArgs} args The required values to create a transaction
* @param {CreateTransactionRegisterRequest} args The required values to create a transaction
* @return {Promise<string>} The transaction ID
*/
public async createRegisterTransaction(args: RegisterTransactionArgs): Promise<string> {
public async createRegisterTransaction(args: CreateTransactionRegisterRequest): Promise<string> {
try {
const { externalId, passkeyDisplayName } = args;
const response = await this.transactionClient.createRegisterTransaction({
appId: this.appId,
createTransactionRegisterRequest: {
externalId,
passkeyDisplayName,
},
createTransactionRegisterRequest: args,
});

return response.transactionId;
} catch (err) {
throw new PassageError('Could not create register transaction', err as ResponseError);
if (err instanceof ResponseError) {
throw await PassageError.fromResponseError('Could not create register transaction', err);
}

throw err;
}
}

/**
* Create a transaction to start a user's authentication process
*
* @param {AuthenticateTransactionArgs} args The required values to create a transaction
* @param {CreateTransactionAuthenticateRequest} args The required values to create a transaction
* @return {Promise<string>} The transaction ID
*/
public async createAuthenticateTransaction(args: AuthenticateTransactionArgs): Promise<string> {
public async createAuthenticateTransaction(args: CreateTransactionAuthenticateRequest): Promise<string> {
try {
const { externalId } = args;
const response = await this.transactionClient.createAuthenticateTransaction({
appId: this.appId,
createTransactionAuthenticateRequest: {
externalId,
},
createTransactionAuthenticateRequest: args,
});

return response.transactionId;
} catch (err) {
throw new PassageError('Could not create authenticate transaction', err as ResponseError);
if (err instanceof ResponseError) {
throw await PassageError.fromResponseError('Could not create authenticate transaction', err);
}

throw err;
}
}

Expand All @@ -139,7 +144,11 @@ export class PassageFlex {

return response.externalId;
} catch (err) {
throw new PassageError('Could not verify nonce', err as ResponseError);
if (err instanceof ResponseError) {
throw await PassageError.fromResponseError('Could not verify nonce', err);
}

throw err;
}
}

Expand All @@ -159,12 +168,16 @@ export class PassageFlex {

const users = response.users;
if (!users.length) {
throw new PassageError('Could not find user with that external ID');
throw PassageError.fromMessage('Could not find user with that external ID');
}

return await this.getUserById(users[0].id);
} catch (err) {
throw new PassageError('Could not fetch user by external ID', err as ResponseError);
if (err instanceof ResponseError) {
throw await PassageError.fromResponseError('Could not fetch user by external ID', err);
}

throw err;
}
}

Expand All @@ -184,7 +197,11 @@ export class PassageFlex {

return response.devices;
} catch (err) {
throw new PassageError("Could not fetch user's devices", err as ResponseError);
if (err instanceof ResponseError) {
throw await PassageError.fromResponseError("Could not fetch user's devices", err);
}

throw err;
}
}

Expand All @@ -206,7 +223,11 @@ export class PassageFlex {

return true;
} catch (err) {
throw new PassageError("Could not delete user's device", err as ResponseError);
if (err instanceof ResponseError) {
throw await PassageError.fromResponseError("Could not delete user's device", err);
}

throw err;
}
}

Expand Down
13 changes: 9 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
export { PassageFlex } from './classes/PassageFlex';
export { PassageError } from './classes/PassageError';
export { PassageError, ErrorStatusText } from './classes/PassageError';
export { AppInfo } from './models/AppInfo';
export { UserInfo } from './models/UserInfo';
export type { PassageConfig } from './types/PassageConfig';
export type { RegisterTransactionArgs } from './types/RegisterTransactionArgs';
export type { AuthenticateTransactionArgs } from './types/AuthenticateTransactionArgs';
export { UserStatus, WebAuthnDevices, WebAuthnIcons, WebAuthnType } from './generated';
export {
CreateTransactionAuthenticateRequest,
CreateTransactionRegisterRequest,
UserStatus,
WebAuthnDevices,
WebAuthnIcons,
WebAuthnType,
} from './generated/models';
6 changes: 0 additions & 6 deletions src/types/AuthenticateTransactionArgs.ts

This file was deleted.

10 changes: 0 additions & 10 deletions src/types/RegisterTransactionArgs.ts

This file was deleted.

48 changes: 33 additions & 15 deletions tests/PassageError.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,45 @@
import { ResponseError } from '../src/generated';
import { PassageError } from '../src/classes/PassageError';
import { faker } from '@faker-js/faker';

describe('PassageError', () => {
test('message only', async () => {
const msg = 'Could not find valid cookie for authentication. You must catch this error.';
const err = new PassageError(msg);
describe('fromMessage', () => {
it('should set PassageError.message to message', async () => {
const expected = faker.string.sample();
const actual = PassageError.fromMessage(expected);

expect(err.message).toEqual(msg);
expect(err.error).toBeUndefined;
expect(actual.message).toEqual(expected);
expect(actual.stack).toBeDefined();
expect(actual.statusText).toBeUndefined();
expect(actual.statusCode).toBeUndefined();
});
});

test('with Response Error', async () => {
const responseError = {
message: 'some error message',
name: 'ResponseError',
} as ResponseError;
describe('fromResponseError', () => {
it('should set PassageError.code and PassageError.status from ResponseError', async () => {
const expectedMessage = 'friendly message';
const expectedResponseCode = faker.string.sample();
const expectedResponseError = 'error body message';

const msg = 'Could not find valid cookie for authentication. You must catch this error';
const err = new PassageError(msg, responseError);
const responseError = {
message: faker.string.sample(),
response: {
status: faker.internet.httpStatusCode(),
json: async () => {
return {
code: expectedResponseCode,
error: expectedResponseError,
};
},
} as Response,
} as ResponseError;

expect(err.message).toEqual(`${msg}: ${responseError.message}`);
expect(err.statusCode).toBe(500);
const actual = await PassageError.fromResponseError(expectedMessage, responseError);

expect(err.error).toBe('some error message');
expect(actual.message).toEqual(`${expectedMessage}: ${expectedResponseError}`);
expect(actual.statusText).toEqual(expectedResponseCode);
expect(actual.statusCode).toEqual(responseError.response.status);
expect(actual.stack).toBeDefined();
});
});
});
Loading

0 comments on commit 9854556

Please sign in to comment.