mirror of
https://github.com/mozilla/fxa.git
synced 2025-12-13 20:36:41 +01:00
Because: * We want a PoC for handling frontend auth errors globally This commit: * adds an error handler callback to auth client Closes #FXA-12605
1771 lines
39 KiB
JavaScript
1771 lines
39 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
'use strict';
|
|
|
|
const inherits = require('util').inherits;
|
|
const OauthError = require('./oauth/error');
|
|
const verror = require('verror');
|
|
|
|
const {
|
|
AUTH_SERVER_ERRNOS: ERRNO,
|
|
AUTH_SERVER_ERRNOS_REVERSE_MAP,
|
|
} = require('fxa-shared/lib/errors');
|
|
|
|
const DEFAULTS = {
|
|
code: 500,
|
|
error: 'Internal Server Error',
|
|
errno: ERRNO.UNEXPECTED_ERROR,
|
|
message: 'Unspecified error',
|
|
info: 'https://mozilla.github.io/ecosystem-platform/api#section/Response-format',
|
|
};
|
|
|
|
const TOO_LARGE =
|
|
/^Payload (?:content length|size) greater than maximum allowed/;
|
|
|
|
const BAD_SIGNATURE_ERRORS = [
|
|
'Bad mac',
|
|
'Unknown algorithm',
|
|
'Missing required payload hash',
|
|
'Payload is invalid',
|
|
];
|
|
|
|
// Payload properties that might help us debug unexpected errors
|
|
// when they show up in production. Obviously we don't want to
|
|
// accidentally send any sensitive data or PII to a 3rd-party,
|
|
// so the set is opt-in rather than opt-out.
|
|
const DEBUGGABLE_PAYLOAD_KEYS = new Set([
|
|
'availableCommands',
|
|
'capabilities',
|
|
'client_id',
|
|
'code',
|
|
'command',
|
|
'duration',
|
|
'excluded',
|
|
'features',
|
|
'messageId',
|
|
'metricsContext',
|
|
'name',
|
|
'preVerified',
|
|
'publicKey',
|
|
'reason',
|
|
'redirectTo',
|
|
'reminder',
|
|
'scope',
|
|
'service',
|
|
'target',
|
|
'to',
|
|
'TTL',
|
|
'ttl',
|
|
'type',
|
|
'unblockCode',
|
|
'verificationMethod',
|
|
]);
|
|
|
|
function AppError(options, extra, headers, error) {
|
|
this.message = options.message || DEFAULTS.message;
|
|
this.isBoom = true;
|
|
this.stack = options.stack;
|
|
if (!this.stack) {
|
|
Error.captureStackTrace(this, AppError);
|
|
}
|
|
if (error) {
|
|
// This is where verror stores the error cause passed in.
|
|
this.jse_cause = error;
|
|
}
|
|
this.errno = options.errno || DEFAULTS.errno;
|
|
this.output = {
|
|
statusCode: options.code || DEFAULTS.code,
|
|
payload: {
|
|
code: options.code || DEFAULTS.code,
|
|
errno: this.errno,
|
|
error: options.error || DEFAULTS.error,
|
|
message: this.message,
|
|
info: options.info || DEFAULTS.info,
|
|
},
|
|
headers: headers || {},
|
|
};
|
|
Object.assign(this.output.payload, extra || {});
|
|
}
|
|
inherits(AppError, verror.WError);
|
|
|
|
AppError.prototype.toString = function () {
|
|
return `Error: ${this.message}`;
|
|
};
|
|
|
|
AppError.prototype.header = function (name, value) {
|
|
this.output.headers[name] = value;
|
|
};
|
|
|
|
AppError.prototype.backtrace = function (traced) {
|
|
this.output.payload.log = traced;
|
|
};
|
|
|
|
/**
|
|
Translates an error from Hapi format to our format
|
|
*/
|
|
AppError.translate = function (request, response) {
|
|
let error;
|
|
if (response instanceof AppError) {
|
|
return response;
|
|
}
|
|
if (OauthError.isOauthRoute(request && request.route.path)) {
|
|
return OauthError.translate(response);
|
|
} else if (response instanceof OauthError) {
|
|
return appErrorFromOauthError(response);
|
|
}
|
|
const payload = response.output.payload;
|
|
const reason = response.reason;
|
|
if (!payload) {
|
|
error = AppError.unexpectedError(request);
|
|
} else if (
|
|
payload.statusCode === 500 &&
|
|
/(socket hang up|ECONNREFUSED)/.test(reason)
|
|
) {
|
|
// A connection to a remote service either was not made or timed out.
|
|
if (response instanceof Error) {
|
|
error = AppError.backendServiceFailure(
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
response
|
|
);
|
|
} else {
|
|
error = AppError.backendServiceFailure();
|
|
}
|
|
} else if (payload.statusCode === 401) {
|
|
// These are common errors generated by Hawk auth lib.
|
|
if (
|
|
payload.message === 'Unknown credentials' ||
|
|
payload.message === 'Invalid credentials'
|
|
) {
|
|
error = AppError.invalidToken(
|
|
`Invalid authentication token: ${payload.message}`
|
|
);
|
|
} else if (payload.message === 'Stale timestamp') {
|
|
error = AppError.invalidTimestamp();
|
|
} else if (payload.message === 'Invalid nonce') {
|
|
error = AppError.invalidNonce();
|
|
} else if (BAD_SIGNATURE_ERRORS.indexOf(payload.message) !== -1) {
|
|
error = AppError.invalidSignature(payload.message);
|
|
} else {
|
|
error = AppError.invalidToken(
|
|
`Invalid authentication token: ${payload.message}`
|
|
);
|
|
}
|
|
} else if (payload.validation) {
|
|
if (payload.message?.includes('is required')) {
|
|
error = AppError.missingRequestParameter(payload.validation.keys[0]);
|
|
} else {
|
|
error = AppError.invalidRequestParameter(payload.validation);
|
|
}
|
|
} else if (payload.statusCode === 413 && TOO_LARGE.test(payload.message)) {
|
|
error = AppError.requestBodyTooLarge();
|
|
} else {
|
|
error = new AppError({
|
|
message: payload.message,
|
|
code: payload.statusCode,
|
|
error: payload.error,
|
|
errno: payload.errno,
|
|
info: payload.info,
|
|
stack: response.stack,
|
|
});
|
|
|
|
if (response.data) {
|
|
error.output.payload.data = JSON.stringify(response.data);
|
|
}
|
|
|
|
if (payload.statusCode >= 500) {
|
|
decorateErrorWithRequest(error, request);
|
|
}
|
|
}
|
|
return error;
|
|
};
|
|
|
|
AppError.mapErrnoToKey = function (error) {
|
|
if (error && error.errno && AUTH_SERVER_ERRNOS_REVERSE_MAP[error.errno]) {
|
|
return AUTH_SERVER_ERRNOS_REVERSE_MAP[error.errno];
|
|
}
|
|
};
|
|
|
|
// Helper functions for creating particular response types.
|
|
|
|
AppError.dbIncorrectPatchLevel = function (level, levelRequired) {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Server Startup',
|
|
errno: ERRNO.SERVER_CONFIG_ERROR,
|
|
message: 'Incorrect Database Patch Level',
|
|
},
|
|
{
|
|
level: level,
|
|
levelRequired: levelRequired,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.accountExists = function (email) {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.ACCOUNT_EXISTS,
|
|
message: 'Account already exists',
|
|
},
|
|
{
|
|
email: email,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.unknownAccount = function (email) {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.ACCOUNT_UNKNOWN,
|
|
message: 'Unknown account',
|
|
},
|
|
{
|
|
email: email,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.incorrectPassword = function (dbEmail, requestEmail) {
|
|
if (dbEmail !== requestEmail) {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INCORRECT_EMAIL_CASE,
|
|
message: 'Incorrect email case',
|
|
},
|
|
{
|
|
email: dbEmail,
|
|
}
|
|
);
|
|
}
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INCORRECT_PASSWORD,
|
|
message: 'Incorrect password',
|
|
},
|
|
{
|
|
email: dbEmail,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.cannotCreatePassword = function () {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.CANNOT_CREATE_PASSWORD,
|
|
message: 'Can not create password, password already set.',
|
|
});
|
|
};
|
|
|
|
AppError.cannotLoginNoPasswordSet = function () {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.UNABLE_TO_LOGIN_NO_PASSWORD_SET,
|
|
message: 'Complete account setup, please reset password to continue.',
|
|
});
|
|
};
|
|
|
|
AppError.unverifiedAccount = function () {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.ACCOUNT_UNVERIFIED,
|
|
message: 'Unconfirmed account',
|
|
});
|
|
};
|
|
|
|
AppError.insufficientAal = function () {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INSUFFICIENT_AAL,
|
|
message: 'Insufficient AAL',
|
|
});
|
|
};
|
|
|
|
AppError.invalidVerificationCode = function (details) {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_VERIFICATION_CODE,
|
|
message: 'Invalid confirmation code',
|
|
},
|
|
details
|
|
);
|
|
};
|
|
|
|
AppError.invalidRequestBody = function () {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_JSON,
|
|
message: 'Invalid JSON in request body',
|
|
});
|
|
};
|
|
|
|
AppError.invalidRequestParameter = function (validation) {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_PARAMETER,
|
|
message: 'Invalid parameter in request body',
|
|
},
|
|
{
|
|
validation: validation,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.missingRequestParameter = function (param) {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.MISSING_PARAMETER,
|
|
message: `Missing parameter in request body${param ? `: ${param}` : ''}`,
|
|
},
|
|
{
|
|
param: param,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.invalidSignature = function (message) {
|
|
return new AppError({
|
|
code: 401,
|
|
error: 'Unauthorized',
|
|
errno: ERRNO.INVALID_REQUEST_SIGNATURE,
|
|
message: message || 'Invalid request signature',
|
|
});
|
|
};
|
|
|
|
AppError.invalidToken = function (message) {
|
|
return new AppError({
|
|
code: 401,
|
|
error: 'Unauthorized',
|
|
errno: ERRNO.INVALID_TOKEN,
|
|
message: message || 'Invalid authentication token in request signature',
|
|
});
|
|
};
|
|
|
|
AppError.invalidMfaToken = function () {
|
|
return new AppError({
|
|
code: 401,
|
|
error: 'Unauthorized',
|
|
errno: ERRNO.INVALID_MFA_TOKEN,
|
|
message: 'Invalid or expired MFA token',
|
|
});
|
|
};
|
|
|
|
AppError.invalidTimestamp = function () {
|
|
return new AppError(
|
|
{
|
|
code: 401,
|
|
error: 'Unauthorized',
|
|
errno: ERRNO.INVALID_TIMESTAMP,
|
|
message: 'Invalid timestamp in request signature',
|
|
},
|
|
{
|
|
serverTime: Math.floor(+new Date() / 1000),
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.invalidNonce = function () {
|
|
return new AppError({
|
|
code: 401,
|
|
error: 'Unauthorized',
|
|
errno: ERRNO.INVALID_NONCE,
|
|
message: 'Invalid nonce in request signature',
|
|
});
|
|
};
|
|
|
|
AppError.unauthorized = function unauthorized(reason) {
|
|
return new AppError(
|
|
{
|
|
code: 401,
|
|
error: 'Unauthorized',
|
|
errno: ERRNO.INVALID_TOKEN,
|
|
message: 'Unauthorized for route',
|
|
},
|
|
{
|
|
detail: reason,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.missingContentLength = function () {
|
|
return new AppError({
|
|
code: 411,
|
|
error: 'Length Required',
|
|
errno: ERRNO.MISSING_CONTENT_LENGTH_HEADER,
|
|
message: 'Missing content-length header',
|
|
});
|
|
};
|
|
|
|
AppError.requestBodyTooLarge = function () {
|
|
return new AppError({
|
|
code: 413,
|
|
error: 'Request Entity Too Large',
|
|
errno: ERRNO.REQUEST_TOO_LARGE,
|
|
message: 'Request body too large',
|
|
});
|
|
};
|
|
|
|
AppError.tooManyRequests = function (
|
|
retryAfter,
|
|
retryAfterLocalized,
|
|
canUnblock
|
|
) {
|
|
if (!retryAfter) {
|
|
retryAfter = 30;
|
|
}
|
|
|
|
const extraData = {
|
|
retryAfter: retryAfter,
|
|
};
|
|
|
|
if (retryAfterLocalized) {
|
|
extraData.retryAfterLocalized = retryAfterLocalized;
|
|
}
|
|
|
|
if (canUnblock) {
|
|
extraData.verificationMethod = 'email-captcha';
|
|
extraData.verificationReason = 'login';
|
|
}
|
|
|
|
const error = new AppError(
|
|
{
|
|
code: 429,
|
|
error: 'Too Many Requests',
|
|
errno: ERRNO.THROTTLED,
|
|
message: 'Client has sent too many requests',
|
|
},
|
|
extraData,
|
|
{
|
|
'retry-after': retryAfter,
|
|
}
|
|
);
|
|
|
|
return error;
|
|
};
|
|
|
|
AppError.requestBlocked = function (canUnblock) {
|
|
let extra;
|
|
if (canUnblock) {
|
|
extra = {
|
|
verificationMethod: 'email-captcha',
|
|
verificationReason: 'login',
|
|
};
|
|
}
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Request blocked',
|
|
errno: ERRNO.REQUEST_BLOCKED,
|
|
message: 'The request was blocked for security reasons',
|
|
},
|
|
extra
|
|
);
|
|
};
|
|
|
|
AppError.serviceUnavailable = function (retryAfter) {
|
|
if (!retryAfter) {
|
|
retryAfter = 30;
|
|
}
|
|
return new AppError(
|
|
{
|
|
code: 503,
|
|
error: 'Service Unavailable',
|
|
errno: ERRNO.SERVER_BUSY,
|
|
message: 'Service unavailable',
|
|
},
|
|
{
|
|
retryAfter: retryAfter,
|
|
},
|
|
{
|
|
'retry-after': retryAfter,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.featureNotEnabled = function (retryAfter) {
|
|
if (!retryAfter) {
|
|
retryAfter = 30;
|
|
}
|
|
return new AppError(
|
|
{
|
|
code: 503,
|
|
error: 'Feature not enabled',
|
|
errno: ERRNO.FEATURE_NOT_ENABLED,
|
|
message: 'Feature not enabled',
|
|
},
|
|
{
|
|
retryAfter: retryAfter,
|
|
},
|
|
{
|
|
'retry-after': retryAfter,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.gone = function () {
|
|
return new AppError({
|
|
code: 410,
|
|
error: 'Gone',
|
|
errno: ERRNO.ENDPOINT_NOT_SUPPORTED,
|
|
message: 'This endpoint is no longer supported',
|
|
});
|
|
};
|
|
|
|
AppError.goneFourOhFour = function () {
|
|
return new AppError({
|
|
code: 404,
|
|
error: 'Gone',
|
|
errno: ERRNO.ENDPOINT_NOT_SUPPORTED,
|
|
message: 'This endpoint is no longer supported',
|
|
});
|
|
};
|
|
|
|
AppError.mustResetAccount = function (email) {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.ACCOUNT_RESET,
|
|
message: 'Account must be reset',
|
|
},
|
|
{
|
|
email: email,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.unknownDevice = function () {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.DEVICE_UNKNOWN,
|
|
message: 'Unknown device',
|
|
});
|
|
};
|
|
|
|
AppError.deviceSessionConflict = function (deviceId) {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.DEVICE_CONFLICT,
|
|
message: 'Session already registered by another device',
|
|
},
|
|
{ deviceId }
|
|
);
|
|
};
|
|
|
|
AppError.invalidUnblockCode = function () {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_UNBLOCK_CODE,
|
|
message: 'Invalid unblock code',
|
|
});
|
|
};
|
|
|
|
AppError.invalidPhoneNumber = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_PHONE_NUMBER,
|
|
message: 'Invalid phone number',
|
|
});
|
|
};
|
|
|
|
AppError.recoveryCodesAlreadyExist = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.RECOVERY_CODES_ALREADY_EXISTS,
|
|
message: 'Recovery codes or a verified TOTP token already exist',
|
|
});
|
|
};
|
|
|
|
AppError.recoveryPhoneNumberAlreadyExists = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.RECOVERY_PHONE_NUMBER_ALREADY_EXISTS,
|
|
message: 'Recovery phone number already exists',
|
|
});
|
|
};
|
|
|
|
AppError.recoveryPhoneNumberDoesNotExist = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.RECOVERY_PHONE_NUMBER_DOES_NOT_EXIST,
|
|
message: 'Recovery phone number does not exist',
|
|
});
|
|
};
|
|
|
|
AppError.recoveryPhoneRemoveMissingRecoveryCodes = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.RECOVERY_PHONE_REMOVE_MISSING_RECOVERY_CODES,
|
|
message:
|
|
'Unable to remove recovery phone, missing backup authentication codes.',
|
|
});
|
|
};
|
|
|
|
AppError.smsSendRateLimitExceeded = () => {
|
|
return new AppError({
|
|
code: 429,
|
|
error: 'Too many requests',
|
|
errno: ERRNO.SMS_SEND_RATE_LIMIT_EXCEEDED,
|
|
message: 'Text message limit reached',
|
|
});
|
|
};
|
|
|
|
AppError.recoveryPhoneRegistrationLimitReached = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.RECOVERY_PHONE_REGISTRATION_LIMIT_REACHED,
|
|
message:
|
|
'Limit reached for number off accounts that can be associated with phone number.',
|
|
});
|
|
};
|
|
|
|
AppError.invalidRegion = (region) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_REGION,
|
|
message: 'Invalid region',
|
|
},
|
|
{
|
|
region,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.unsupportedLocation = (country) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.UNSUPPORTED_LOCATION,
|
|
message: 'Location is not supported according to our Terms of Service.',
|
|
},
|
|
{
|
|
country,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.currencyCountryMismatch = (currency, country) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_REGION,
|
|
message: 'Funding source country does not match plan currency.',
|
|
},
|
|
{
|
|
currency,
|
|
country,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.currencyCurrencyMismatch = (currencyA, currencyB) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_CURRENCY,
|
|
message: `Changing currencies is not permitted.`,
|
|
},
|
|
{
|
|
currencyA,
|
|
currencyB,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.billingAgreementExists = (customerId) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.BILLING_AGREEMENT_EXISTS,
|
|
message: `Billing agreement already on file for this customer.`,
|
|
},
|
|
{
|
|
customerId,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.missingPaypalPaymentToken = (customerId) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.MISSING_PAYPAL_PAYMENT_TOKEN,
|
|
message: `PayPal payment token is missing.`,
|
|
},
|
|
{
|
|
customerId,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.missingPaypalBillingAgreement = (customerId) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.MISSING_PAYPAL_BILLING_AGREEMENT,
|
|
message: `PayPal billing agreement is missing for the existing subscriber.`,
|
|
},
|
|
{
|
|
customerId,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.invalidMessageId = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_MESSAGE_ID,
|
|
message: 'Invalid message id',
|
|
});
|
|
};
|
|
|
|
AppError.messageRejected = (reason, reasonCode) => {
|
|
return new AppError(
|
|
{
|
|
code: 500,
|
|
error: 'Internal Server Error',
|
|
errno: ERRNO.MESSAGE_REJECTED,
|
|
message: 'Message rejected',
|
|
},
|
|
{
|
|
reason,
|
|
reasonCode,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.emailComplaint = (bouncedAt) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.BOUNCE_COMPLAINT,
|
|
message: 'Email account sent complaint',
|
|
},
|
|
{
|
|
bouncedAt,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.emailBouncedHard = (bouncedAt) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.BOUNCE_HARD,
|
|
message: 'Email account hard bounced',
|
|
},
|
|
{
|
|
bouncedAt,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.emailBouncedSoft = (bouncedAt) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.BOUNCE_SOFT,
|
|
message: 'Email account soft bounced',
|
|
},
|
|
{
|
|
bouncedAt,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.emailExists = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.EMAIL_EXISTS,
|
|
message: 'Email already exists',
|
|
});
|
|
};
|
|
|
|
AppError.cannotDeletePrimaryEmail = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.EMAIL_DELETE_PRIMARY,
|
|
message: 'Can not delete primary email',
|
|
});
|
|
};
|
|
|
|
AppError.unverifiedSession = function () {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.SESSION_UNVERIFIED,
|
|
message: 'Unconfirmed session',
|
|
});
|
|
};
|
|
|
|
AppError.yourPrimaryEmailExists = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.USER_PRIMARY_EMAIL_EXISTS,
|
|
message: 'Can not add secondary email that is same as your primary',
|
|
});
|
|
};
|
|
|
|
AppError.verifiedPrimaryEmailAlreadyExists = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.VERIFIED_PRIMARY_EMAIL_EXISTS,
|
|
message: 'Email already exists',
|
|
});
|
|
};
|
|
|
|
AppError.verifiedSecondaryEmailAlreadyExists = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.VERIFIED_SECONDARY_EMAIL_EXISTS,
|
|
message: 'Email already exists',
|
|
});
|
|
};
|
|
|
|
// This error is thrown when someone attempts to add a secondary email
|
|
// that is the same as the primary email of another account, but the account
|
|
// was recently created ( < 24hrs).
|
|
AppError.unverifiedPrimaryEmailNewlyCreated = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.UNVERIFIED_PRIMARY_EMAIL_NEWLY_CREATED,
|
|
message: 'Email already exists',
|
|
});
|
|
};
|
|
|
|
AppError.unverifiedPrimaryEmailHasActiveSubscription = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.UNVERIFIED_PRIMARY_EMAIL_HAS_ACTIVE_SUBSCRIPTION,
|
|
message: 'Account for this email has an active subscription',
|
|
});
|
|
};
|
|
|
|
AppError.maxSecondaryEmailsReached = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.MAX_SECONDARY_EMAILS_REACHED,
|
|
message: 'You have reached the maximum allowed secondary emails',
|
|
});
|
|
};
|
|
|
|
AppError.alreadyOwnsEmail = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Conflict',
|
|
errno: ERRNO.ACCOUNT_OWNS_EMAIL,
|
|
message: 'This email already exists on your account',
|
|
});
|
|
};
|
|
|
|
AppError.cannotLoginWithSecondaryEmail = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.LOGIN_WITH_SECONDARY_EMAIL,
|
|
message: 'Sign in with this email type is not currently supported',
|
|
});
|
|
};
|
|
|
|
AppError.unknownSecondaryEmail = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.SECONDARY_EMAIL_UNKNOWN,
|
|
message: 'Unknown email',
|
|
});
|
|
};
|
|
|
|
AppError.cannotResetPasswordWithSecondaryEmail = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.RESET_PASSWORD_WITH_SECONDARY_EMAIL,
|
|
message: 'Reset password with this email type is not currently supported',
|
|
});
|
|
};
|
|
|
|
AppError.invalidSigninCode = function () {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_SIGNIN_CODE,
|
|
message: 'Invalid signin code',
|
|
});
|
|
};
|
|
|
|
AppError.cannotChangeEmailToUnverifiedEmail = function () {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.CHANGE_EMAIL_TO_UNVERIFIED_EMAIL,
|
|
message: 'Can not change primary email to an unconfirmed email',
|
|
});
|
|
};
|
|
|
|
AppError.cannotChangeEmailToUnownedEmail = function () {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.CHANGE_EMAIL_TO_UNOWNED_EMAIL,
|
|
message:
|
|
'Can not change primary email to an email that does not belong to this account',
|
|
});
|
|
};
|
|
|
|
AppError.cannotLoginWithEmail = function () {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.LOGIN_WITH_INVALID_EMAIL,
|
|
message: 'This email can not currently be used to login',
|
|
});
|
|
};
|
|
|
|
AppError.cannotResendEmailCodeToUnownedEmail = function () {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.RESEND_EMAIL_CODE_TO_UNOWNED_EMAIL,
|
|
message:
|
|
'Can not resend email code to an email that does not belong to this account',
|
|
});
|
|
};
|
|
|
|
AppError.cannotSendEmail = function (isNewAddress) {
|
|
if (!isNewAddress) {
|
|
return new AppError({
|
|
code: 500,
|
|
error: 'Internal Server Error',
|
|
errno: ERRNO.FAILED_TO_SEND_EMAIL,
|
|
message: 'Failed to send email',
|
|
});
|
|
}
|
|
return new AppError({
|
|
code: 422,
|
|
error: 'Unprocessable Entity',
|
|
errno: ERRNO.FAILED_TO_SEND_EMAIL,
|
|
message: 'Failed to send email',
|
|
});
|
|
};
|
|
|
|
AppError.invalidTokenVerficationCode = function (details) {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_TOKEN_VERIFICATION_CODE,
|
|
message: 'Invalid token confirmation code',
|
|
},
|
|
details
|
|
);
|
|
};
|
|
|
|
AppError.expiredTokenVerficationCode = function (details) {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.EXPIRED_TOKEN_VERIFICATION_CODE,
|
|
message: 'Expired token confirmation code',
|
|
},
|
|
details
|
|
);
|
|
};
|
|
|
|
AppError.totpTokenAlreadyExists = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.TOTP_TOKEN_EXISTS,
|
|
message: 'TOTP token already exists for this account.',
|
|
});
|
|
};
|
|
|
|
AppError.totpTokenDoesNotExist = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.TOTP_SECRET_DOES_NOT_EXIST,
|
|
message: 'TOTP secret does not exist for this account.',
|
|
});
|
|
};
|
|
|
|
AppError.totpTokenNotFound = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.TOTP_TOKEN_NOT_FOUND,
|
|
message: 'TOTP token not found.',
|
|
});
|
|
};
|
|
|
|
AppError.recoveryCodeNotFound = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.RECOVERY_CODE_NOT_FOUND,
|
|
message: 'Backup authentication code not found.',
|
|
});
|
|
};
|
|
|
|
AppError.unavailableDeviceCommand = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.DEVICE_COMMAND_UNAVAILABLE,
|
|
message: 'Unavailable device command.',
|
|
});
|
|
};
|
|
|
|
AppError.recoveryKeyNotFound = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.RECOVERY_KEY_NOT_FOUND,
|
|
message: 'Account recovery key not found.',
|
|
});
|
|
};
|
|
|
|
AppError.recoveryKeyInvalid = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.RECOVERY_KEY_INVALID,
|
|
message: 'Account recovery key is not valid.',
|
|
});
|
|
};
|
|
|
|
AppError.totpRequired = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.TOTP_REQUIRED,
|
|
message:
|
|
'This request requires two step authentication enabled on your account.',
|
|
});
|
|
};
|
|
|
|
AppError.recoveryKeyExists = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.RECOVERY_KEY_EXISTS,
|
|
message: 'Account recovery key already exists.',
|
|
});
|
|
};
|
|
|
|
AppError.unknownClientId = (clientId) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.UNKNOWN_CLIENT_ID,
|
|
message: 'Unknown client_id',
|
|
},
|
|
{
|
|
clientId,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.incorrectClientSecret = (clientId) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INCORRECT_CLIENT_SECRET,
|
|
message: 'Incorrect client_secret',
|
|
},
|
|
{
|
|
clientId,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.staleAuthAt = (authAt) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.STALE_AUTH_AT,
|
|
message: 'Stale auth timestamp',
|
|
},
|
|
{
|
|
authAt,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.notPublicClient = function notPublicClient() {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.NOT_PUBLIC_CLIENT,
|
|
message: 'Not a public client',
|
|
});
|
|
};
|
|
|
|
AppError.redisConflict = () => {
|
|
return new AppError({
|
|
code: 409,
|
|
error: 'Conflict',
|
|
errno: ERRNO.REDIS_CONFLICT,
|
|
message: 'Redis WATCH detected a conflicting update',
|
|
});
|
|
};
|
|
|
|
AppError.incorrectRedirectURI = (redirectUri) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INCORRECT_REDIRECT_URI,
|
|
message: 'Incorrect redirect URI',
|
|
},
|
|
{
|
|
redirectUri,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.unknownAuthorizationCode = (code) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.UNKNOWN_AUTHORIZATION_CODE,
|
|
message: 'Unknown authorization code',
|
|
},
|
|
{
|
|
code,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.mismatchAuthorizationCode = (code, clientId) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.MISMATCH_AUTHORIZATION_CODE,
|
|
message: 'Mismatched authorization code',
|
|
},
|
|
{
|
|
code,
|
|
clientId,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.expiredAuthorizationCode = (code, expiredAt) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.EXPIRED_AUTHORIZATION_CODE,
|
|
message: 'Expired authorization code',
|
|
},
|
|
{
|
|
code,
|
|
expiredAt,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.invalidResponseType = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_RESPONSE_TYPE,
|
|
message: 'Invalid response_type',
|
|
});
|
|
};
|
|
|
|
AppError.invalidScopes = (invalidScopes) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_SCOPES,
|
|
message: 'Requested scopes are not allowed',
|
|
},
|
|
{
|
|
invalidScopes,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.missingPkceParameters = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.MISSING_PKCE_PARAMETERS,
|
|
message: 'Public clients require PKCE OAuth parameters',
|
|
});
|
|
};
|
|
|
|
AppError.invalidPkceChallenge = (pkceHashValue) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_PKCE_CHALLENGE,
|
|
message: 'Public clients require PKCE OAuth parameters',
|
|
},
|
|
{
|
|
pkceHashValue,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.invalidPromoCode = (promotionCode) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_PROMOTION_CODE,
|
|
message: 'Invalid promotion code',
|
|
},
|
|
{
|
|
promotionCode,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.unknownCustomer = (uid) => {
|
|
return new AppError(
|
|
{
|
|
code: 404,
|
|
error: 'Not Found',
|
|
errno: ERRNO.UNKNOWN_SUBSCRIPTION_CUSTOMER,
|
|
message: 'Unknown customer',
|
|
},
|
|
{
|
|
uid,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.unknownSubscription = (subscriptionId) => {
|
|
return new AppError(
|
|
{
|
|
code: 404,
|
|
error: 'Not Found',
|
|
errno: ERRNO.UNKNOWN_SUBSCRIPTION,
|
|
message: 'Unknown subscription',
|
|
},
|
|
{
|
|
subscriptionId,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.unknownAppName = (appName) => {
|
|
return new AppError(
|
|
{
|
|
code: 404,
|
|
error: 'Not Found',
|
|
errno: ERRNO.IAP_UNKNOWN_APPNAME,
|
|
message: 'Unknown app name',
|
|
},
|
|
{
|
|
appName,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.unknownSubscriptionPlan = (planId) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.UNKNOWN_SUBSCRIPTION_PLAN,
|
|
message: 'Unknown subscription plan',
|
|
},
|
|
{
|
|
planId,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.rejectedSubscriptionPaymentToken = (message, paymentError) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.REJECTED_SUBSCRIPTION_PAYMENT_TOKEN,
|
|
message,
|
|
},
|
|
paymentError
|
|
);
|
|
};
|
|
|
|
AppError.rejectedCustomerUpdate = (message, paymentError) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.REJECTED_CUSTOMER_UPDATE,
|
|
message,
|
|
},
|
|
paymentError
|
|
);
|
|
};
|
|
|
|
AppError.subscriptionAlreadyCancelled = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.SUBSCRIPTION_ALREADY_CANCELLED,
|
|
message: 'Subscription has already been cancelled',
|
|
});
|
|
};
|
|
|
|
AppError.invalidPlanUpdate = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_PLAN_UPDATE,
|
|
message: 'Subscription plan is not a valid update',
|
|
});
|
|
};
|
|
|
|
AppError.paymentFailed = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.PAYMENT_FAILED,
|
|
message: 'Payment method failed',
|
|
});
|
|
};
|
|
|
|
AppError.subscriptionAlreadyChanged = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.SUBSCRIPTION_ALREADY_CHANGED,
|
|
message: 'Subscription has already been cancelled',
|
|
});
|
|
};
|
|
|
|
AppError.subscriptionAlreadyExists = () => {
|
|
return new AppError({
|
|
code: 409,
|
|
error: 'Already subscribed',
|
|
errno: ERRNO.SUBSCRIPTION_ALREADY_EXISTS,
|
|
message: 'User already subscribed.',
|
|
});
|
|
};
|
|
|
|
AppError.userAlreadySubscribedToProduct = () => {
|
|
return new AppError({
|
|
code: 409,
|
|
error: 'Already subscribed to product with different plan',
|
|
errno: ERRNO.SUBSCRIPTION_ALREADY_EXISTS,
|
|
message: 'User already subscribed to product with different plan.',
|
|
});
|
|
};
|
|
|
|
AppError.iapInvalidToken = (error) => {
|
|
const extra = error ? [{}, undefined, error] : [];
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.IAP_INVALID_TOKEN,
|
|
message: `Invalid IAP token${error?.message ? `: ${error.message}` : ''}`,
|
|
},
|
|
...extra
|
|
);
|
|
};
|
|
|
|
AppError.iapPurchaseConflict = (error) => {
|
|
const extra = error ? [{}, undefined, error] : [];
|
|
return new AppError(
|
|
{
|
|
code: 403,
|
|
error: 'Forbidden',
|
|
errno: ERRNO.IAP_PURCHASE_ALREADY_REGISTERED,
|
|
message: 'Purchase has been registered to another user.',
|
|
},
|
|
...extra
|
|
);
|
|
};
|
|
|
|
AppError.invalidInvoicePreviewRequest = (error, message, priceId, customer) => {
|
|
const extra = error ? [{}, undefined, error] : [];
|
|
return new AppError(
|
|
{
|
|
code: 500,
|
|
error: 'Internal Server Error',
|
|
errno: ERRNO.INVALID_INVOICE_PREVIEW_REQUEST,
|
|
message,
|
|
},
|
|
{
|
|
priceId,
|
|
customer,
|
|
},
|
|
...extra
|
|
);
|
|
};
|
|
|
|
AppError.iapInternalError = (error) => {
|
|
const extra = error ? [{}, undefined, error] : [];
|
|
return new AppError(
|
|
{
|
|
code: 500,
|
|
error: 'Internal Server Error',
|
|
errno: ERRNO.IAP_INTERNAL_OTHER,
|
|
message: 'IAP Internal Error',
|
|
},
|
|
...extra
|
|
);
|
|
};
|
|
|
|
AppError.insufficientACRValues = (foundValue) => {
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INSUFFICIENT_ACR_VALUES,
|
|
message:
|
|
'Required Authentication Context Reference values could not be satisfied',
|
|
},
|
|
{
|
|
foundValue,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.unknownRefreshToken = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.REFRESH_TOKEN_UNKNOWN,
|
|
message: 'Unknown refresh token',
|
|
});
|
|
};
|
|
|
|
AppError.invalidOrExpiredOtpCode = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_EXPIRED_OTP_CODE,
|
|
message: 'Invalid or expired confirmation code',
|
|
});
|
|
};
|
|
|
|
AppError.thirdPartyAccountError = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.THIRD_PARTY_ACCOUNT_ERROR,
|
|
message: 'Could not login with third party account, please try again later',
|
|
});
|
|
};
|
|
|
|
AppError.backendServiceFailure = (service, operation, extra, error) => {
|
|
if (extra) {
|
|
return new AppError(
|
|
{
|
|
code: 500,
|
|
error: 'Internal Server Error',
|
|
errno: ERRNO.BACKEND_SERVICE_FAILURE,
|
|
message: 'System unavailable, try again soon',
|
|
},
|
|
{
|
|
service,
|
|
operation,
|
|
...extra,
|
|
},
|
|
{},
|
|
error
|
|
);
|
|
}
|
|
return new AppError(
|
|
{
|
|
code: 500,
|
|
error: 'Internal Server Error',
|
|
errno: ERRNO.BACKEND_SERVICE_FAILURE,
|
|
message: 'System unavailable, try again soon',
|
|
},
|
|
{
|
|
service,
|
|
operation,
|
|
},
|
|
{},
|
|
error
|
|
);
|
|
};
|
|
|
|
AppError.disabledClientId = (clientId, retryAfter) => {
|
|
if (!retryAfter) {
|
|
retryAfter = 30;
|
|
}
|
|
return new AppError(
|
|
{
|
|
code: 503,
|
|
error: 'Client Disabled',
|
|
errno: ERRNO.DISABLED_CLIENT_ID,
|
|
message: 'This client has been temporarily disabled',
|
|
},
|
|
{
|
|
clientId,
|
|
retryAfter,
|
|
},
|
|
{
|
|
'retry-after': retryAfter,
|
|
}
|
|
);
|
|
};
|
|
|
|
AppError.internalValidationError = (op, data, error) => {
|
|
return new AppError(
|
|
{
|
|
code: 500,
|
|
error: 'Internal Server Error',
|
|
errno: ERRNO.INTERNAL_VALIDATION_ERROR,
|
|
message: 'An internal validation check failed.',
|
|
},
|
|
{
|
|
op,
|
|
data,
|
|
},
|
|
{},
|
|
error
|
|
);
|
|
};
|
|
|
|
AppError.unexpectedError = (request) => {
|
|
const error = new AppError({});
|
|
decorateErrorWithRequest(error, request);
|
|
return error;
|
|
};
|
|
|
|
AppError.missingSubscriptionForSourceError = (op, data) =>
|
|
new AppError(
|
|
{
|
|
code: 500,
|
|
error: 'Missing subscription for source',
|
|
errno: ERRNO.UNKNOWN_SUBSCRIPTION_FOR_SOURCE,
|
|
message: 'Failed to find a subscription associated with Stripe source.',
|
|
},
|
|
{
|
|
op,
|
|
data,
|
|
}
|
|
);
|
|
|
|
AppError.accountCreationRejected = () => {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.ACCOUNT_CREATION_REJECTED,
|
|
message: 'Account creation rejected.',
|
|
});
|
|
};
|
|
|
|
AppError.subscriptionPromotionCodeNotApplied = (error, message) => {
|
|
const extra = error ? [{}, undefined, error] : [];
|
|
return new AppError(
|
|
{
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.SUBSCRIPTION_PROMO_CODE_NOT_APPLIED,
|
|
message,
|
|
},
|
|
...extra
|
|
);
|
|
};
|
|
|
|
AppError.invalidCloudTaskEmailType = function () {
|
|
return new AppError({
|
|
code: 400,
|
|
error: 'Bad Request',
|
|
errno: ERRNO.INVALID_CLOUDTASK_EMAILTYPE,
|
|
message: 'Invalid email type',
|
|
});
|
|
};
|
|
|
|
function decorateErrorWithRequest(error, request) {
|
|
if (request) {
|
|
error.output.payload.request = {
|
|
// request.app.devices and request.app.metricsContext are async, so can't be included here
|
|
acceptLanguage: request.app.acceptLanguage,
|
|
locale: request.app.locale,
|
|
userAgent: request.app.ua,
|
|
method: request.method,
|
|
path: request.path,
|
|
query: request.query,
|
|
payload: scrubPii(request.payload),
|
|
headers: scrubHeaders(request.headers),
|
|
};
|
|
}
|
|
}
|
|
|
|
function scrubPii(payload) {
|
|
if (!payload) {
|
|
return;
|
|
}
|
|
|
|
return Object.entries(payload).reduce((scrubbed, [key, value]) => {
|
|
if (DEBUGGABLE_PAYLOAD_KEYS.has(key)) {
|
|
scrubbed[key] = value;
|
|
}
|
|
|
|
return scrubbed;
|
|
}, {});
|
|
}
|
|
|
|
function scrubHeaders(headers) {
|
|
const scrubbed = { ...headers };
|
|
delete scrubbed['x-forwarded-for'];
|
|
return scrubbed;
|
|
}
|
|
|
|
function appErrorFromOauthError(err) {
|
|
switch (err.errno) {
|
|
case 101:
|
|
return AppError.unknownClientId(err.clientId);
|
|
case 102:
|
|
return AppError.incorrectClientSecret(err.clientId);
|
|
case 103:
|
|
return AppError.incorrectRedirectURI(err.redirectUri);
|
|
case 104:
|
|
return AppError.invalidToken();
|
|
case 105:
|
|
return AppError.unknownAuthorizationCode(err.code);
|
|
case 106:
|
|
return AppError.mismatchAuthorizationCode(err.code, err.clientId);
|
|
case 107:
|
|
return AppError.expiredAuthorizationCode(err.code, err.expiredAt);
|
|
case 108:
|
|
return AppError.invalidToken();
|
|
case 109:
|
|
return AppError.invalidRequestParameter(err.validation);
|
|
case 110:
|
|
return AppError.invalidResponseType();
|
|
case 114:
|
|
return AppError.invalidScopes(err.invalidScopes);
|
|
case 116:
|
|
return AppError.notPublicClient();
|
|
case 117:
|
|
return AppError.invalidPkceChallenge(err.pkceHashValue);
|
|
case 118:
|
|
return AppError.missingPkceParameters();
|
|
case 119:
|
|
return AppError.staleAuthAt(err.authAt);
|
|
case 120:
|
|
return AppError.insufficientACRValues(err.foundValue);
|
|
case 121:
|
|
return AppError.invalidRequestParameter('grant_type');
|
|
case 122:
|
|
return AppError.unknownRefreshToken();
|
|
case 201:
|
|
return AppError.serviceUnavailable(err.retryAfter);
|
|
case 202:
|
|
return AppError.disabledClientId(err.clientId);
|
|
default:
|
|
return err;
|
|
}
|
|
}
|
|
|
|
// Maintain list of errors that should not be sent to Sentry
|
|
const IGNORED_ERROR_NUMBERS = [
|
|
ERRNO.BOUNCE_HARD,
|
|
ERRNO.BOUNCE_SOFT,
|
|
ERRNO.BOUNCE_COMPLAINT,
|
|
];
|
|
|
|
/**
|
|
* Prevents errors from being captured in sentry.
|
|
*
|
|
* @param {Error} error An error with an error number. Note that errors of type vError will
|
|
* use the underlying jse_cause error if possible.
|
|
*/
|
|
function ignoreErrors(error) {
|
|
if (!error) {
|
|
return;
|
|
}
|
|
|
|
// Prefer jse_cause, but fallback to top level error if needed
|
|
const statusCode =
|
|
determineStatusCode(error.jse_cause) || determineStatusCode(error);
|
|
|
|
const errno = error.jse_cause?.errno || error.errno;
|
|
|
|
// Ignore non 500 status codes and specific error numbers
|
|
return statusCode < 500 || IGNORED_ERROR_NUMBERS.includes(errno);
|
|
}
|
|
|
|
/**
|
|
* Given an error tries to determine the HTTP status code associated with the error.
|
|
* @param {*} error
|
|
* @returns
|
|
*/
|
|
function determineStatusCode(error) {
|
|
if (!error) {
|
|
return;
|
|
}
|
|
|
|
return error.statusCode || error.output?.statusCode || error.code;
|
|
}
|
|
|
|
module.exports = AppError;
|
|
module.exports.ERRNO = ERRNO;
|
|
module.exports.ignoreErrors = ignoreErrors;
|