packages/fxa-auth-server/lib/error.js (1,558 lines of code) (raw):

/* 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.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.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'; } return new AppError( { code: 429, error: 'Too Many Requests', errno: ERRNO.THROTTLED, message: 'Client has sent too many requests', }, extraData, { 'retry-after': retryAfter, } ); }; 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.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.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;